一组用于调试、比较和测试应用程序数据结构的工具。
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([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
和 Swift Testing 的 #expect(_ == _)
都允许您断言两个值相等,如果它们不相等,测试套件将失败并显示消息
var other = user
other.name += "!"
XCTAssertEqual(user, other)
#expect(user == other)
XCTAssertEqual failed: ("User(favoriteNumbers: [42, 1729], id: 2, name: "Blob")") is not equal to ("User(favoriteNumbers: [42, 1729], id: 2, name: "Blob!")")
Expectation failed: (user → User(favoriteNumbers: [42, 1729], id: 2, name: "Blob")) == (other → User(favoriteNumbers: [42, 1729], id: 2, name: "Blob!"))
不幸的是,这些失败消息很难在视觉上解析和理解。需要花一些时间在消息中查找才能看到唯一的区别是名称末尾的感叹号。如果类型更复杂,由嵌套结构和大型集合组成,则问题会变得更糟。
这个库还附带一个 expectNoDifference
函数来缓解这些问题。它的工作方式类似于 XCTAssertEqual
和 #expect(_ == _)
,除了失败消息使用格式良好的差异来准确显示两个值之间的差异
expectNoDifference(user, other)
expectNoDifference failed: …
User(
favoriteNumbers: […],
id: 2,
- name: "Blob"
+ name: "Blob!"
)
(First: −, Second: +)
此函数提供 expectNoDifference
的逆函数:它通过评估给定操作之前和之后的给定表达式,然后比较结果,来断言值具有一组更改。
例如,给定一个非常简单的计数器结构,我们可以针对其递增功能编写测试
struct Counter {
var count = 0
var isOdd = false
mutating func increment() {
self.count += 1
self.isOdd.toggle()
}
}
var counter = Counter()
expectDifference(counter) {
counter.increment()
} changes: {
$0.count = 1
$0.isOdd = true
}
如果 changes
没有详尽地描述所有更改的字段,则断言将失败。
通过省略操作,您可以通过仅描述要在 changes
闭包中断言的字段,来编写针对值的“非详尽”断言
counter.increment()
expectDifference(counter) {
$0.count = 1
// Don't need to further describe how `isOdd` has changed
}
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")
就像那样,没有令牌数据将被写入转储。
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: "1.0.0")
]
Custom Dump API 的最新文档可在此处 获取。
此库在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE。