自定义转储
用于调试、比较和测试应用程序数据结构的工具集合。
动机
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
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"
)
)
]
diff
使用 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)
]
expectNoDifference
XCTest 的 XCTAssertEqual
和 Swift Testing 的 #expect(_ == _)
都允许你断言两个值相等,如果不相等,测试套件将失败并显示消息:
var other = user
other.name += "!"
XCTAssertEqual(user, other)
#expect(user == other)
XCTAssertEqual失败:"User(favoriteNumbers: [42, 1729], id: 2, name: "Blob")"不等于"User(favoriteNumbers: [42, 1729], id: 2, name: "Blob!")"
期望失败:(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失败:…
User(
favoriteNumbers: [...],
id: 2,
- name: "Blob"
+ name: "Blob!"
)
(第一个:-,第二个:+)
expectDifference
该函数提供了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
没有详尽描述所有changed字段,断言将失败。
通过省略操作,你可以在changes
闭包中只描述你想断言的字段,从而对一个值进行"非详尽"断言:
counter.increment()
expectDifference(counter) {
$0.count = 1
// 不需要进一步描述`isOdd`如何变化
}
自定义
Custom Dump提供了几种重要的方式来自定义数据类型的转储方式:CustomDumpStringConvertible
、CustomDumpReflectable
和CustomDumpRepresentable
。
CustomDumpStringConvertible
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
CustomDumpReflectable
协议提供了一种更全面的方法来将类型转储为更结构化的输出。它允许你构造一个自定义镜像,描述应该转储的结构。你可以省略、添加和替换字段,甚至更改结构转储的"显示样式"。
例如,假设你有一个表示状态的结构,它在内存中保存了一个不应该写入日志的安全令牌。你可以通过提供一个省略此字段的镜像来从customDump
中省略令牌:
struct LoginState: CustomDumpReflectable {
var username: String
var token: String
var customDumpMirror: Mirror {
.init(
self,
children: [
"username": self.username,
// 从日志中省略令牌
],
displayStyle: .struct
)
}
}
customDump(
LoginState(
username: "blob",
token: "secret"
)
)
LoginState(username: "blob")
就这样,转储中不会写入令牌数据。
CustomDumpRepresentable
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。