用于调试、比较和测试应用程序数据结构的工具集合。
Swift 附带一个很棒的工具,可以将任何值的内容转储到字符串中,它被称为 dump
。它将一个值的所有字段和子字段打印成一个树状描述
struct User {
var favoriteNumbers: [Int]
var id: Int
var name: String
}
let user = User(
favoriteNumbers: [42, 1729],
id: 2,
name: "Blob"
)
dump(user)
▿ User
▿ favoriteNumbers: 2 elements
- 42
- 1729
- id: 2
- name: "Blob"
这非常有用,并且非常适合构建调试工具,以可视化应用程序运行时值中保存的数据,但有时其输出并不理想。
例如,dump 字典会导致冗长的输出,难以阅读(另请注意,键是无序的)
dump([1: "one", 2: "two", 3: "three"])
▿ 3 key/value pairs
▿ (2 elements)
- key: 2
- value: "two"
▿ (2 elements)
- key: 3
- value: "three"
▿ (2 elements)
- key: 1
- value: "one"
类似地,枚举也有非常冗长的输出
dump(Result<Int, Error>.success(42))
▿ Swift.Result<Swift.Int, Swift.Error>.success
- success: 42
处理深度嵌套的结构时,阅读起来更加困难
dump([1: Result<User, Error>.success(user)])
▿ 1 key/value pair
▿ (2 elements)
- key: 1
▿ value: Swift.Result<User, Swift.Error>.success
▿ success: User
▿ favoriteNumbers: 2 elements
- 42
- 1729
- id: 2
- name: "Blob"
有时 dump
根本不打印有用的信息,例如从 Objective-C 导入的枚举
import UserNotifications
dump(UNNotificationSetting.disabled)
- __C.UNNotificationSetting
因此,虽然 dump
函数可能很方便,但它通常过于粗糙。这是 customDump
函数的动机。
customDump
函数模仿 dump
的行为,但提供更精细的嵌套结构输出,从而优化可读性。 例如,结构体以更接近 Swift 中结构体语法的格式转储,数组以每个元素的索引转储
import CustomDump
customDump(user)
User(
favoriteNumbers: [
[0]: 42,
[1]: 1729
],
id: 2,
name: "Blob"
)
字典以更紧凑的格式转储,模仿 Swift 的语法,并自动对键进行排序
customDump([1: "one", 2: "two", 3: "three"])
[
1: "one",
2: "two",
3: "three"
]
类似地,枚举也以更紧凑、更易读的格式转储
customDump(Result<Int, Error>.success(42))
Result.success(42)
深度嵌套的结构具有简化的树形结构
customDump([1: Result<User, Error>.success(user)])
[
1: Result.success(
User(
favoriteNumbers: [
[0]: 42,
[1]: 1729
],
id: 2,
name: "Blob"
)
)
]
使用 customDump
函数的输出,我们可以构建一种非常轻量级的方法来以文本方式比较 Swift 中的任意两个值
var other = user
other.favoriteNumbers[1] = 91
print(diff(user, other)!)
User(
favoriteNumbers: [
[0]: 42,
- [1]: 1729
+ [1]: 91
],
id: 2,
name: "Blob"
)
此外,当结构的某些部分没有更改时,例如大型集合中的单个元素发生更改时,会进行额外的工作以最大限度地减少差异的大小
let users = (1...5).map {
User(
favoriteNumbers: [$0],
id: $0,
name: "Blob \($0)"
)
}
var other = users
other.append(
.init(
favoriteNumbers: [42, 1729],
id: 100,
name: "Blob Sr."
)
)
print(diff(users, other)!)
[
… (4 unchanged),
+ [4]: User(
+ favoriteNumbers: [
+ [0]: 42,
+ [1]: 1729
+ ],
+ id: 100,
+ name: "Blob Sr."
+ )
]
对于一个真实的用例,我们修改了 Apple 的 Landmarks 教程应用程序,以打印收藏地标之前和之后的状态
[
[0]: Landmark(
id: 1001,
name: "Turtle Rock",
park: "Joshua Tree National Park",
state: "California",
description: "This very large formation lies south of the large Real Hidden Valley parking lot and immediately adjacent to (south of) the picnic areas.",
- isFavorite: true,
+ isFavorite: false,
isFeatured: true,
category: Category.rivers,
imageName: "turtlerock",
coordinates: Coordinates(…)
),
… (11 unchanged)
]
XCTest
中的 XCTAssertEqual
函数允许你断言两个值是否相等,如果不相等,测试套件将失败并显示一条消息
var other = user
other.name += "!"
XCTAssertEqual(user, other)
XCTAssertEqual failed: ("User(favoriteNumbers: [42, 1729], id: 2, name: "Blob")") is not equal to ("User(favoriteNumbers: [42, 1729], id: 2, name: "Blob!")")
不幸的是,此失败消息很难在视觉上进行解析和理解。需要花费一些时间在消息中寻找才能发现唯一的区别是名称末尾的感叹号。如果类型更复杂,包含嵌套结构和大型集合,则问题会变得更糟。
此库还附带一个 XCTAssertNoDifference
函数来缓解这些问题。它的工作方式类似于 XCTAssertEqual
,只不过失败消息使用格式良好的 diff 来准确显示两个值之间的差异
XCTAssertNoDifference(user, other)
XCTAssertNoDifference failed: …
User(
favoriteNumbers: […],
id: 2,
− name: "Blob"
+ name: "Blob!"
)
(First: −, Second: +)
Custom Dump 提供了几种重要的方式来定制数据类型的转储方式:CustomDumpStringConvertible
、CustomDumpReflectable
和 CustomDumpRepresentable
。
CustomDumpStringConvertible
协议提供了一种将类型转换为原始字符串的简单方法,用于转储。它最适合具有简单、非嵌套内部表示的类型,并且通常其输出适合单行,例如日期、UUID、URL 等
extension URL: CustomDumpStringConvertible {
public var customDumpDescription: String {
"URL(\(self.absoluteString))"
}
}
customDump(URL(string: "https://www.pointfree.co/")!)
URL(https://www.pointfree.co/)
Custom Dump 也在内部使用此协议,为从 Objective-C 导入的枚举提供更有用的输出
import UserNotifications
print("dump:")
dump(UNNotificationSetting.disabled)
print("customDump:")
customDump(UNNotificationSetting.disabled)
dump:
- __C.UNNotificationSetting
customDump:
UNNotificationSettings.disabled
遇到一个无法很好打印的 Objective-C 枚举? 请参阅此 README 的 贡献 部分,以帮助提交修复。
CustomDumpReflectable
协议提供了一种更全面的方式,将类型转储为更结构化的输出。 它允许你构建一个自定义镜像,描述应该转储的结构。 你可以省略、添加和替换字段,甚至更改结构的“显示样式”的转储方式。
例如,假设你有一个表示状态的结构体,该状态在内存中保存一个永远不应写入日志的安全令牌。 你可以通过提供一个省略此字段的镜像来从 customDump
中省略令牌
struct LoginState: CustomDumpReflectable {
var username: String
var token: String
var customDumpMirror: Mirror {
.init(
self,
children: [
"username": self.username,
// omit token from logs
],
displayStyle: .struct
)
}
}
customDump(
LoginState(
username: "blob",
token: "secret"
)
)
LoginState(username: "blob")
就像这样,不会将任何令牌数据写入 dump。
CustomDumpRepresentable
协议允许你返回任何值用于转储。 这对于展平包装器类型的转储表示非常有用。 例如,类型安全的标识符可能希望直接转储其原始值
struct ID: RawRepresentable {
var rawValue: String
}
extension ID: CustomDumpRepresentable {
var customDumpValue: Any {
self.rawValue
}
}
customDump(ID(rawValue: "deadbeef")
"deadbeef"
Apple 生态系统中有很多类型无法转储为格式良好的字符串。 特别是,从 Objective-C 导入的所有枚举都转储为不太有用的字符串
import UserNotifications
dump(UNNotificationSetting.disabled)
- __C.UNNotificationSetting
因此,我们已经使 大量 Apple 的类型符合 CustomDumpStringConvertible
协议,以便它们打印出更合理的描述。 如果你遇到无法打印有用信息的类型,我们将很乐意接受 PR,使这些类型符合 CustomDumpStringConvertible
。
你可以通过将 Custom Dump 添加为软件包依赖项来将其添加到 Xcode 项目中。
如果要在 SwiftPM 项目中使用 Custom Dump,只需将其添加到 Package.swift
中的 dependencies
子句中即可
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.3.0")
]
Custom Dump API 的最新文档可在此处获得:此处。
此库在 MIT 许可下发布。 有关详细信息,请参见 LICENSE。