printing vs dumping

温馨提示:本文最后更新于2024-08-20 16:59:44,某些文章具有时效性,若有错误或已失效,请在下方留言

有什么问题?

print() 方法有一定的缺点,为了说明这个问题,我们定义一个 Footballer 结构体,结构体包含队员的姓名、位置以及俱乐部,后边的两者是不同的类型。

enum Position {
    case goal, defence, midifield, attack
}

struct Club {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

struct Footballer {
    var name: String
    var club: Club
    var position: Position
    
    init(name: String, club: Club, position: Position) {
        self.name = name
        self.club = club
        self.position = position
    }
}

我们现在可以继续使用这个类型

let richmond = Club(name: "AFC Richmond")
let roy = Footballer(name: "Roy Kent", club: richmond, position: .midifield)
print(roy)

打印的结果如下

Footballer(name: “Roy Kent”, club: PrintingData.Club(name: “AFC Richmond”), position: PrintingData.Position.midifi

如果将 Footballer 定义为类,而非结构体

class Footballer {

打印的输出内容如下

PrintingData.Footballer

介绍 dump()

Swift 有几种将信息打印到调试输出的方法,但真正最有用的方法是 dump(),因为它使用对象的 镜像 来确定对象所包含的内容。

镜像 允许对 Swift 类型进行自省,这意味着当我们创建一个类型的镜像时,我们可以读取该类型中的所有属性名和值–我们可以对属性及其值进行字面上的循环,这正是 dump() 所要做的。

更妙的是,dump() 会递归遍历我们要转储的类型中的所有类型,因此我们可以得到一个非常漂亮的层次结构。

Footballer 为类时,将 print 变为 dump,如下

dump(roy)

打印的内容,如下

▿ PrintingData.Footballer #0
  - name: "Roy Kent"
  ▿ club: PrintingData.Club
    - name: "AFC Richmond"
  - position: PrintingData.Position.midifield

关于其它类型

这种额外的信息非常受欢迎,在许多其他类型中也会出现。

print(1...10)
dump(1...10)

打印的内容,如下

1...10
▿ ClosedRange(1...10)
  - lowerBound: 1
  - upperBound: 10

你会发现 dump() 更加清晰:它会打印出我们正在使用的类型,并说明这两个值的含义。

对数组来说也更清晰:

let names = ["Taylor", "Justin", "Adele"]
print(names)
dump(names)

打印的内容,如下

["Taylor", "Justin", "Adele"]
▿ 3 elements
  - "Taylor"
  - "Justin"
  - "Adele"

你会看到 dump() 会打印出数组的计数,并将每个元素放在自己的一行中。

它还能为 Foundation 类型提供 print() 无法提供的信息。例如,尝试使用 IndexSet

let numbers = IndexSet([1, 2, 3, 5, 7, 9])
print(numbers)
dump(numbers)

打印的内容,如下

6 indexes
▿ 6 indexes
  ▿ ranges: 4 elements
    ▿ Range(1..<4)
      - lowerBound: 1
      - upperBound: 4
    ▿ Range(5..<6)
      - lowerBound: 5
      - upperBound: 6
    ▿ Range(7..<8)
      - lowerBound: 7
      - upperBound: 8
    ▿ Range(9..<10)
      - lowerBound: 9
      - upperBound: 10

print() 只会告诉你 IndexSet 中有多少个数字,而 dump() 则会细分实际内容。

自定义输出

我们可以向 dump() 传递一些参数,以自定义其工作方式。

最简单的是 name 参数,它允许我们为被转储的内容添加一个字符串名称,以方便识别:

dump(roy, name: "Roy")

打印的内容,如下

▿ Roy: PrintingData.Footballer #0
  - name: "Roy Kent"
  ▿ club: PrintingData.Club
    - name: "AFC Richmond"
  - position: PrintingData.Position.midifield

我想说的后两个是 maxDepthmaxItems。这两个是一起工作的:

  • maxDepth 决定 dump() 将导航到子对象的哪个深度。例如,如果将 roy 对象的最大深度设为 1,就意味着将打印其属性,但 dump() 不会打印其俱乐部的 name 属性。
  • maxItems 决定打印的属性总数。这是所有对象的总计数,递归计算。

我们可以将 maxDepth 设为 2,将 maxItems 设为 4:

dump(roy, name: "Roy", maxDepth: 2, maxItems: 4)

打印的内容,如下

▿ Roy: PrintingData.Footballer #0
  - name: "Roy Kent"
  ▿ club: PrintingData.Club
    - name: "AFC Richmond"
    (1 more child)

这将打印 Roy 以及他的姓名、俱乐部和俱乐部名称,但不会打印他的位置,因为已达到 maxItems

不过,最有趣的参数是我们把转储的数据写到哪里,print()dump() 都有一个额外的参数,可以将它们的输出引向其他地方–任何知道如何接受字符串并以某种方式处理它的地方。

class Logger: TextOutputStream {
    private let fileHandle: FileHandle
    
    init(url: URL) throws {
        // more code to come
    }
    
    func write(_ string: String) {
        let data = Data(string.utf8)
        fileHandle.write(data)
    }
    
    deinit {
        try? fileHandle.close()
    }
}

这会将所有输入字符串写入一个打开的文件句柄。其中唯一真正的工作是在文件不存在的情况下创建文件,然后将其赋值给 fileHandle 属性。

// more code to come 替换为以下内容:

let fm = FileManager.default
let path = url.path()

if fm.fileExists(atPath: path) == false {
    fm.createFile(atPath: path, contents: nil)
}

fileHandle = try FileHandle(forWritingTo: url)
try fileHandle.seekToEnd()

现在,我们可以像这样直接将数据打印或转储到文本文件中:

let url = URL.homeDirectory.appending(path: "output.log")
var logger = try Logger(url: url)
dump(roy, to: &logger, name: "Roy", maxDepth: 2, maxItems: 4)

output.log 日志文件内容

output.log 日志文件

参考

  1. 文章来自: Hacking With Swift Plus
© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容