自定义 Dump

CI

用于调试、比较和测试应用程序数据结构的工具集合。

动机

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

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)
  ]

XCTAssertNoDifference

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 提供了几种重要的方式来定制数据类型的转储方式:CustomDumpStringConvertibleCustomDumpReflectableCustomDumpRepresentable

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,
        // omit token from logs
      ],
      displayStyle: .struct
    )
  }
}

customDump(
  LoginState(
    username: "blob",
    token: "secret"
  )
)
LoginState(username: "blob")

就像这样,不会将任何令牌数据写入 dump。

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 项目中。

https://github.com/pointfreeco/swift-custom-dump

如果要在 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