普通视图

发现新文章,点击刷新页面。
昨天以前NSHipster

numericCast(_:)

作者 Mattt
2018年10月15日 00:00

每个人都曾将编程比喻成其他事物。

类比成木工、编织或者园艺。又或者可能类比成解决问题、讲故事或者制作艺术品。毫无疑问,编程与写作也很像;问题是更像诗歌还是散文。如果编程像音乐的话,不管怎么样它都应该是爵士乐。

或许对我们每天所做工作最近似的类比来自中东民间故事:打开任何版本的《一千零一夜 (أَلْف لَيْلَة وَلَيْلَة) 》,你会找到对一种被称作镇尼杰尼精灵或者 🧞 的神奇生物的描述。不管你怎么称呼它们,你一定熟悉它们实现愿望的习惯,和必然会引起的不幸。

从许多方面来看,电脑是抽象的愿望满足机的物理体现。像精灵一样,电脑会开心的执行任何你告诉它要做的事,而不会考虑你真正的意图是什么。之后当你意识到自己的错误时,就已经太晚了。

作为一个 Swift 开发者,很有可能你遇到过整数类型转换错误并想着「我希望这些警告赶紧消失,代码能编译通过」。

如果这听起来很熟悉,那你会对学习 numbericCast(_:) 感到高兴,它是 Swift Standard Libray 中一个小小的实用函数,有可能正是你所希望的。但是请小心提出你的愿望,它有可能马上会成真。


让我们从消除觉得 numericCast(_:) 有什么魔法开始,通过查看它的实现

public func numericCast<T : BinaryInteger, U : BinaryInteger>(_ x: T) -> U {
        return U(x)
        }
        

(像从我们有关 Never 的文章里学到的一样,极小量的 Swift 代码也能有巨大的作用。)

Swift 4 推出的 BinaryInteger 协议,作为语言中整个数字实现的一部分。它提供了与整数工作的统一接口,包括有符号和无符号,还有所有的结构和大小。

当你将一个整数值转换为另一个类型时,另一个类型有可能无法表示这个值。这会发生在你尝试将一个有符号整数转换成一个无符号整数时(比如将 -42 转换为 UInt)或者数值超过了目标类型所能表示的范围时(比如 UInt8 只能表示 0255 之间的数字)。

BinaryInteger 为整数类型转换定义了四种策略,每一种在处理超出范围的值时都有不同行为:

  • 范围检查转换init(_:)): 遇到超出范围的值时触发运行时错误
  • 准确转换init?(exactly:)): 遇到超出范围的值时返回 nil
  • 钳制转换init(clamping:)): 遇到超出范围的值时使用最近可表示的值
  • 位模式转换init(truncatingIfNeeded:)): 截断至目标整数类型宽度

正确的转换策略取决于使用时的情况。有些时候,希望能钳制数值到可表示的范围;有些时候,最好不要获取到任何值。对于 numbericCast(_:) 来说,它为了方便使用了范围检查转换。缺点就是使用超过范围的数值调用这个函数会导致运行时错误(具体来说,在 -O-Onone 时陷入溢出错误)。

字面地思考,批判地思考

在更进一步之前,让我们先来谈论一下整数字面量。

我们在之前的文章讨论过,Swift 提供了一个方便且可扩展的方式来在源代码中表示值。当和语言中的类型推断一起使用时,它们通常「可以工作」……这样一切都很好,但是当它们「无法工作」时就非常令人困惑了。

考虑下面的例子,有符号整型数组和无符号整型数组使用同样的字面量初始化:

let arrayOfInt: [Int] = [1, 2, 3]
        let arrayOfUInt: [UInt] = [1, 2, 3]
        

尽管它们好像是相等的,但我们不能做下面例子中的事情:

arrayOfInt as [UInt] // Error: Cannot convert value of type '[Int]' to type '[UInt]' in coercion
        

解决这个问题的一种方式是,将 numericCast 函数作为参数传入 map(_:)

arrayOfInt.map(numericCast) as [UInt]
        

这样等同于直接传入 UInt 范围检查构造器:

arrayOfInt.map(UInt.init)
        

让我们再看一次这个例子,这次使用稍微不同的数值:

let arrayOfNegativeInt: [Int] = [-1, -2, -3]
        arrayOfNegativeInt.map(numericCast) as [UInt] // 🧞‍ Fatal error: Negative value is not representable
        

作为一个编译时类型功能的运行时近似物,numericCast(_:) 更像是 as! 而不是 asas?

将这个和传入精确转换构造器 init?(exactly:) 的结果相比:

let arrayOfNegativeInt: [Int] = [-1, -2, -3]
        arrayOfNegativeInt.map(UInt.init(exactly:)) // [nil, nil, nil]
        

numericCast(_:),像它内在的范围检查转换一样,是一个钝器,当你决定使用它时,明白你在权衡什么是非常重要的。

正确的代价

在 Swift 中,通常指导是为整数值使用 Int(且为浮点值使用 Double),除非有非常好的理由来使用更具体的类型。尽管 Collectioncount 在定义上是非负的,但我们使用 Int 而不是 UInt。因为在与其他 API 交互时转换来转换去类型的代价要比更精确类型带来的好处要大。同样的原因,用 Int 来表示小数字几乎总是会更好,比如工作日数字,尽管它所有的可能值用一个 8 位整型存储都绰绰有余。

理解这个实践最好的方式就是在 Swift 里和 C API 对话几分钟。

古老且低级的 C API 里充斥着体系结构相关的类型定义和细微调整过的值存储空间。独立的来看,它们是可管理的。但从像头文件到指针这些互操作性麻烦上看,它们对某些问题可能会是一个断点(我不是在说调试中那种)。

当你看红色看到烦,只想要编译通过时,numericCast(_:) 就在那等着你。

编译的随机性

很多人应该会熟悉官方文档中的例子

SE-0202 之前,(在苹果的平台上)Swift 中生成随机数的标准实践需要引入 Darwin 框架然后调用 arc4random_uniform(3) 函数:

uint32_t arc4random_uniform(uint32_t __upper_bound)
        

在 Swift 中使用 arc4random 需要进行不止一次而是两次类型转换:一是上限参数(IntUInt32),二是返回值(UInt32Int):

import Darwin
        func random(in range: Range<Int>) -> Int {
        return Int(arc4random_uniform(UInt32(range.count))) + range.lowerBound
        }
        

真恶心。

通过使用 numericCast(_:),我们可以让代码更可读一些,尽管也会变长一点:

import Darwin
        func random(in range: Range<Int>) -> Int {
        return numericCast(arc4random_uniform(numericCast(range.count))) + range.lowerBound
        }
        

在这里 numericCast(_:) 没有做任何类型合适的构造器做不到的事情。它的作用是指明这个转换是敷衍的——为了让代码编译需要做的最少的事情。

不过从前言有关精灵的事情中学到,我们应该谨慎的对待我们的愿望。

经过仔细检查,上面对例子中对 numericCast(_:) 的使用有一个明显的缺陷:当值超过 UInt32.max 时会造成崩溃!

random(in: 0..<0x1_0000_0000) // 🧞‍ Fatal error: Not enough bits to represent the passed value
        

如果我们查看现在 Int.random(in: 0...10) 在 Swift Standard Library 中的实现,可以看到其使用了钳制转换而不是类型检查转换。并且从一个随机字节缓冲区中取值而不是委托给像 arc4random_uniform 这样的简便函数。


编译通过的代码和正确的代码是不一样的。但有时候需要通过前者来最终获得后者。审慎的使用,numericCast(_:) 会是一个方便且能快速解决问题的工具。和类型转换构造器相比它还有表明潜在异常行为的好处。

根本上来说,编程就是准确描述我们想要怎么样——通常伴随艰苦的细节。并没有一个和精灵似的「做正确的事情」 CPU 指令(就算有的话,我们能信赖它吗?)。幸好,Swift 可以让我们比其他很多语言更安全和简洁的做这些事情。老实说,谁还能要求更多呢?

Swift Property Observers

作者 Mattt
2018年8月20日 00:00

到了 20 世纪 30 年代,Rube Goldberg 已成为家喻户晓的名字,与“自营餐巾” 等漫画中描绘的奇异复杂和异想天开的发明同义。大约在同一时期,阿尔伯特·爱因斯坦对尼尔斯·玻尔量子力学的普遍解释进行了批判,并从中提出了“鬼魅似的远距作用”这一词汇。

近一个世纪之后,现代软件开发已经被视为可能成为 Goldbergian 装置的典范——通过量子计算机相信我们会越来越接近这个鬼魅的领域。

作为软件开发人员,我们提倡尽可能减少代码中的远程操作。这是根据一些众所周知的规范法则得出的,如单一职责原则最少意外原则得墨忒耳定律。尽管它们可能会对代码产生一定的副作用,但更多的时候这些原则能使代码逻辑变得清晰。

这是本周文章的焦点 Swift 属性观察器,它是系统内置的,比模型 - 视图 - 视图模型(MVVM)、函数响应式编程(FRP)这些更正式的解决方案更轻量。


Swift 中有两种属性:存储属性,它们将状态和对象相关联;计算属性,则根据该状态执行计算。例如,

struct S {
        // 存储属性
        var stored: String = "stored"
        // 计算属性
        var computed: String {
        return "computed"
        }
        }
        

当你声明一个存储属性,你可以使用闭包定义一个 属性观察器,该闭包中的代码会在属性被设值的时候执行。willSet 观察器会在属性被赋新值之前被运行,didSet 观察器则会在属性被赋新值之后运行。无论新值是否等于属性的旧值它们都会被执行。

struct S {
        var stored: String {
        willSet {
        print("willSet was called")
        print("stored is now equal to \(self.stored)")
        print("stored will be set to \(newValue)")
        }
        didSet {
        print("didSet was called")
        print("stored is now equal to \(self.stored)")
        print("stored was previously set to \(oldValue)")
        }
        }
        }
        

例如,运行下面的代码在控制台的输出如下:

var s = S(stored: "first")
        s.stored = "second"
        
  • willSet was called
  • stored is now equal to first
  • stored will be set to second
  • didSet was called
  • stored is now equal to second
  • stored was previously set to first

Swift 的属性观察器从一开始就是语言的一部分。为了更好地理解其原理,让我们快速了解一下它在 Objective-C 中的工作原理。

Objective-C 中的属性

从某种意义上说,Objective-C 中的所有属性都是被计算出来的。每次通过点语法访问属性时,都会转换为等效的 getter 或 setter 方法调用。这些调用最终被编译成消息发送,随后再执行读取或写入实例变量的方法。

// 点语法访问
        person.name = @"Johnny";
        // ...等价于
        [person setName:@"Johnny"];
        // ...它被编译成
        objc_msgSend(person, @selector(setName:), @"Johnny");
        // ...最终实现
        person->_name = @"Johnny";
        

编程过程中我们通常想要避免引入副作用,因为它会导致程序的行为难以推断。但很多 Objective-C 开发者已经依赖于这种特性,他们会根据需要在 getter 或 setter 中注入各种额外的行为。

Swift 的属性设计使这些模式更加标准化,并对装饰状态访问(存储属性)的副作用和重定向状态访问(计算属性)的副作用进行了区分。对于存储属性,willSetdidSet 观察器将替换你在 ivar 访问时的代码。对于计算属性,getset 访问器可能会替换在 Objective-C 中实现的一些 @dynamic 属性。

正因为如此,我们才可以获取更一致的语义,并更好地保证键值观察(KVO)和键值编码(KVC)等属性交互机制。


那么你可以使用 Swift 属性观察器做些什么呢?以下是一些供你参考的想法:


标准化或验证值

有时,你希望对类型接受的值增加额外的约束。

例如,你正在开发一个和政府机构对接的应用程序,你需要保证用户填写了所有的必填项并且不包含非法的值才能提交表单。

如果一个表单要求名称字段使用大写字母且不使用重音符号,你可以使用 didSet 属性观察器自动去除重音符号并转化为大写。

var name: String? {
        didSet {
        self.name = self.name?
        .applyingTransform(.stripDiacritics,
        reverse: false)?
        .uppercased()
        }
        }
        

幸运的是在观察器内部设置属性不会触发额外的回调,所以上面的代码中不会产生无限循环。我们之所以不使用 willSet 观察器是因为即使我们在其回调中进行任何赋值,都会在属性被赋予 newValue 时覆盖。

虽然这种方法可以解决一次性问题,但像这样需要重复使用的业务逻辑可以封装到一个类型中。

更好的设计是创建一个 NormalizedText 类型,它封装了要以这种形式输入的文本的规则:

struct NormalizedText {
        enum Error: Swift.Error {
        case empty
        case excessiveLength
        case unsupportedCharacters
        }
        static let maximumLength = 32
        private(set) var value: String
        init(_ string: String) throws {
        if string.isEmpty {
        throw Error.empty
        }
        guard let value = string.applyingTransform(.stripDiacritics,
        reverse: false)?
        .uppercased(),
        value.canBeConverted(to: .ascii)
        else {
        throw Error.unsupportedCharacters
        }
        guard value.count < NormalizedText.maximumLength else {
        throw Error.excessiveLength
        }
        self.value = value
        }
        }
        

一个可抛出异常的初始化方法可以向调用者发送错误信息,这是 didSet 观察器无法做到的。现在面对兰韦尔普尔古因吉尔戈格里惠尔恩德罗布尔兰蒂西利奥戈戈戈赫约翰尼 这样的麻烦制造者,我们能为他做些什么!(换言之,以合理的方式传达错误比提供无效的数据更好)

传播依赖状态

属性观察器的另一个潜在用例是将状态传播到依赖于视图控制器的组件。

考虑下面的 Track 模型示例和一个呈现它的 TrackViewController

struct Track {
        var title: String
        var audioURL: URL
        }
        class TrackViewController: UIViewController {
        var player: AVPlayer?
        var track: Track? {
        willSet {
        self.player?.pause()
        }
        didSet {
        guard let track = self.track else {
        return
        }
        self.title = track.title
        let item = AVPlayerItem(url: track.audioURL)
        self.player = AVPlayer(playerItem: item)
        self.player?.play()
        }
        }
        }
        

当视图控制器的 track 属性被赋值,以下事情会自动发生:

  1. 之前轨道的音频都会暂停
  2. 视图控制器的 title 会被设置为新轨道对象的标题
  3. 新轨道对象的音频信息会被加载并播放

很酷, 对吗?

你甚至可以像捕鼠记 中描绘的场景 一样,将行为与多个观察属性级联起来。


当然,观察器也存在一定的副作用,它使得有些复杂的行为难以被推断,这是我们在编程中需要避免的。今后在使用这一特性的同时也需要注意这一点。

然而,在这摇摇欲坠的抽象塔的顶端,一定限度的系统混乱是诱人的,有时是值得的。一直遵循规则的是波尔理论而非爱因斯坦。

Hashable / Hasher

作者 Mattt
2018年8月13日 00:00

当你在苹果商店预约天才吧服务后,相关工作人员会帮你登记并且安排特定的服务时间,在被带到座位上之后,工作人员会记录你的身份信息并添加到服务队列当中。

根据一份来自零售店某位前员工的报告表示,对于顾客的描述有着严格的指导方针。他们的外貌特征如:年龄、性别、种族、身高都没有被使用 —— 甚至连头发的颜色都没有被使用。而是通过顾客的着装来描述,例如“黑色的高领毛衣,牛仔裤和眼镜”。

这种描述顾客的方式和编程中的哈希函数有很多共同之处。同许多优秀的哈希函数一样,它是连续和易计算的,可用于快速找到你正在寻找的内容(或者人)。我想你肯定也觉得这样比队列要好用多了。

这周我们的主题是 Hashable 和相关的新类型 Hasher。它们共同组成了 Swift 最受喜爱的两个集合类 DictionarySet 的基础功能。


假设你有一个可以比较相等性的对象列表。要在这个列表中找到一个特定的对象,你需要遍历这个列表的元素,直到找到匹配项为止。随着你向列表中添加更多的元素时,需要找到其中任何一个元素所需的平均时间是线性级的(O(n))。

如果将这些对象存储在一个集合中,理论上可以在常量级时间(O(1))内找到它们中的任何一个 - 也就是说,在一个包含 10 个元素的集合中查找或在一个包含 10000* 个元素的集合中查找所需的时间是一样的。这是怎么回事呢?因为集合不是按顺序存储对象的,而是将对象内容计算的哈希值作为索引存储。当在集合中查找对象时,可以使用相同的哈希函数计算新的哈希值然后查找对象存储位置。

* 如果两个不同的对象具有相同的哈希值时,会产生哈希冲突。当发生哈希冲突时,它们将存储在该地址对应的列表中。对象之间发生冲突的概率越高,哈希集合的性能就会更加线性增长。

Hashable

在 Swift 中,Array 为列表提供了标准的接⼝,Set 为集合提供了标准的接⼝。如果要将对象存储到 Set 中,就要遵循 Hashable 协议及其扩展协议 Equatable。Swift 的标准映射接口 Dictionary 对它的关联类型 Key 也需要遵循 Hashable 协议及其扩展协议。

在 Swift 之前的版本中,为了让自定义类型能支持 SetDictionary 存储需要写⼤量的 样板代码

以下面的 Color 类型为例,Color 使⽤了 8 位整型值来表示红,绿,蓝色值:

struct Color {
        let red: UInt8
        let green: UInt8
        let blue: UInt8
        }
        

要符合 Equatable 的要求,你需要提供一个 == 操作符的实现。要符合 Hashable 的要求,你需要提供⼀个名为 hashValue 的计算属性:

// Swift < 4.1
        extension Color: Equatable {
        static func ==(lhs: Color, rhs: Color) -> Bool {
        return lhs.red == rhs.red &&
        lhs.green == rhs.green &&
        lhs.blue == rhs.blue
        }
        }
        extension Color: Hashable {
        var hashValue: Int {
        return self.red.hashValue ^
        self.green.hashValue ^
        self.blue.hashValue
        }
        }
        

对于大多数开发者⽽⾔,实现 Hashable 只是为了能尽快让要做的事情步入正轨,因此他们会对所有的存储属性使⽤逻辑异或操作,并在某一天调用它。

然⽽这种实现的一个缺陷是高哈希冲突率。由于逻辑异或满⾜交换率,像⻘色和⻩色这样不同的颜色也会发⽣哈希冲突:

// Swift < 4.2
        let cyan = Color(red: 0x00, green: 0xFF, blue: 0xFF)
        let yellow = Color(red: 0xFF, green: 0xFF, blue: 0x00)
        cyan.hashValue == yellow.hashValue // true, collision
        

大多数时候这样做不会出问题;现代计算机已经足够强大以至于你很难意识到性能的衰减,除⾮你的实现细节存在⼤量问题。

但这并不是说这些细节⽆关紧要 —— 它们往往极其重要。稍后会详细介绍。

自动合成 Hashable 实现

从 Swift 4.1 开始,如果某个类型在声明时遵循了 EquatableHashable 协议并且它的成员变量同时也满足了这些协议,编译器会为其自动合成 EquatableHashable 的实现。

除了大大的提高了开发人员的开发效率以外,还可以大幅减少代码的数量。比如,我们之前 Color 的例子 —— 现在是最开始代码量的 1/3 :

// Swift >= 4.1
        struct Color: Hashable {
        let red: UInt8
        let green: UInt8
        let blue: UInt8
        }
        

尽管对语言进行了明显的改进,但还是有一些实现细节有着无法忽视的问题。

在 Swift Evolution 提案 SE-0185: 合成 EquatableHashable 的实现 中, Tony Allevato 给哈希函数提供了这个注释:

哈希函数的选择应该作为实现细节,而不是设计中的固定部分;因此,使用者不应该依赖于编译器自动生成的 Hashable 函数的具体特征。最可能的实现是在每个成员的哈希值上调用标准库中的 _mixInt 函数,然后将他们逻辑异或(\^),如同目前 Collection 类型的哈希方式一样。

幸运的是,Swift 不需要多久就能解决这个问题。我们将在下一个版本得到答案:

Hasher

Swift 4.2 通过引入 Hasher 类型并采用新的通用哈希函数进一步优化 Hashable

在 Swift Evolution 提案 SE-0206: Hashable 增强 中:

使用一个好的哈希函数时,简单的查找,插入,删除操作都只需要常量级时间即可完成。然而,如果没有为当前数据选择一个合适的哈希函数,这些操作的预期时间就会和哈希表中存储的数据数量成正比。

正如 Karoy LorenteyVincent Esche 所指出的那样,SetDictionary 等基于哈希的集合主要特点是它们能够在常量级时间内查找值。如果哈希函数不能产生一个均匀的值分布,这些集合实际上就变成了链表。

Swift 4.2 中的哈希函数是基于伪随机函数族 SipHash 实现的,比如 SipHash-1-3 and SipHash-2-4,分别在每个消息块异或哈希之后执行一次 round + 三次 final round,或两次 round + 四次 final round。(译者注:这里的 round 指的是伪随机数变化。具体实现看 _round)

现在,如果你要自定义类型实现 Hashable 的方式,可以重写 hash(into:) 方法而不是 hashValuehash(into:) 通过传递了一个 Hasher 引用对象,然后通过这个对象调用 combine(_:) 来添加类型的必要状态信息。

// Swift >= 4.2
        struct Color: Hashable {
        let red: UInt8
        let green: UInt8
        let blue: UInt8
        // Synthesized by compiler
        func hash(into hasher: inout Hasher) {
        hasher.combine(self.red)
        hasher.combine(self.green)
        hasher.combine(self.blue)
        }
        // Default implementation from protocol extension
        var hashValue: Int {
        var hasher = Hasher()
        self.hash(into: &hasher)
        return hasher.finalize()
        }
        }
        

通过抽象隔离底层的位操作细节,开发人员可以利用 Swift 内置的哈希函数,这样可以避免再现我们原有的基于逻辑异或实现的冲突:

// Swift >= 4.2
        let cyan = Color(red: 0x00, green: 0xFF, blue: 0xFF)
        let yellow = Color(red: 0xFF, green: 0xFF, blue: 0x00)
        cyan.hashValue == yellow.hashValue // false, no collision
        

自定义哈希函数

默认情况下,Swift 使用通用的哈希函数将字节序列缩减为一个整数。

但是,你可以使用你项目中自定义的哈希函数来改进这个缩减的问题。比如,如果你正在编写一个程序来玩国际象棋或者棋盘游戏,你可以使用 Zobrist hashing 来快速的存储游戏的状态。

避免哈希泛滥(Hash-Flooding)

选择像 SipHash 这样的加密算法有助于防止哈希泛滥的 DoS 攻击,这种攻击会尝试生成哈希冲突,并试图强制实施哈希数据结构最坏的情况,最终导致程序慢下来。这在 2010 年初引发了一系列的网络问题

为了使事情变的更加安全,Hasher 会在每次启动应用程序时生成一个随机种子值,使得哈希值更难以预测。


编程类比的挑战在于它们通过边界情况规范反社会行为。

当我们能够考虑到攻击者所有可能利用来达到某种险恶目的的情况时,这时能体现出我们优秀工程师的品质 —— 比如哈希泛滥的 DoS 攻击。在现实生活中,这么做我们需要冒着失败的风险去应用这些 AFK(Away From Keyboard)知识。

也就是说…亲爱的读者,我不希望你和你的朋友下次穿一样的衣服去当地苹果商店的天才吧中制造混乱和不和谐。

请不要这么做。

相反的,希望你有下面的收获:

当你在天才吧等候的时候,和穿同样颜色衣服的人站得远一点。这会让每个人做事都变得容易得多。

Never

作者 Mattt
2018年7月30日 00:00

“Never”是一个约定,表示一件事在过去或未来的任何时段都不会发生。它是时间轴上的一种逻辑上的不可能,在任何方向延展开去都没有可能。这就是为什么在代码中看到 这样的注释 会特别让人不安。

// this will never happen
        

所有编译器的教科书都会告诉你,这样一句注释不能也不会对编译出的代码产生任何影响。墨菲定理 告诉你并非如此,注释以下的代码一定会被触发。

那 Swift 是如何在这种无法预测的、混乱的开发过程中保证安全呢?答案难以置信:“什么都不做”,以及“崩溃”。


使用 Never 替换 @noreturn 修饰符,是由 Joe GroffSE-0102: “Remove @noreturn attribute and introduce an empty Never type” 中提出的。

在 Swift 3 之前,那些要中断执行的函数,比如 fatalError(_:file:line:)abort()exit(_:),需要使用 @noreturn 修饰符来声明,这会告诉编译器,执行完成之后不用返回到调用方。

// Swift < 3.0
        @noreturn func fatalError(_ message: () -> String = String(),
        file: StaticString = #file,
        line: UInt = #line)
        

从 Swift 3 开始,fatalError 和它的相关函数都被声明为返回 Never 类型。

// Swift >= 3.0
        func fatalError(_ message: @autoclosure () -> String = String(),
        file: StaticString = #file,
        line: UInt = #line) -> Never
        

作为一个注释的替代品,它肯定是很复杂的,对吗?NO!事实上,恰恰相反,Never 可以说是整个 Swift 标准库中最简单的一个类型:

enum Never {}
        

无实例类型(Uninhabited Types)

Never 是一个 无实例Uninhabited)类型,也就是说它没有任何值。或者换句话说,无实例类型是无法被构建的。

在 Swift 中,没有定义任何 case 的枚举是最常见的一种无实例类型。跟结构体和类不同,枚举没有初始化方法。跟协议也不同,枚举是一个具体的类型,可以包含属性、方法、泛型约束和嵌套类型。正因如此,Swift 标准库广泛使用无实例的枚举类型来做诸如 定义命名空间 以及 标识类型的含义 之类的事情。

Never 并不这样。它没有什么花哨的东西,它的特别之处就在于,它就是它自己(或者说,它什么都不是)。

试想一个返回值为无实例类型的函数:因为无实例类型没有任何值,所以这个函数无法正常的返回。(它要如何生成这个返回值呢?)所以,这个函数要么停止运行,要么无休止的一直运行下去。

消除泛型中的不可能状态

从理论角度上说,Never 确实很有意思,但它在实际应用中又能帮我们做什么呢?

做不了什么,至少在 SE-0215: Conform Never to Equatable and Hashable 推出以前,做不了什么。

Matt Diephouse 在提案中解释了为什么让这个令人费解的类型去遵守 Equatable 和其他协议:

Never 在表示不可能执行的代码方面非常有用。大部分人熟悉它,是因为它是 fatalError 等方法的返回值,但 Never 在泛型方面也非常有用。比如说,一个 Result 类型可以使用 Never 作为它的 Value,表示某种东西一直是错误的,或者使用 Never 作为它的 Error,表示某种东西一直不是错误的。

Swift 没有标准的 Result 类型,大部分情况下它们是这个样子的:

enum Result<Value, Error: Swift.Error> {
        case success(Value)
        case failure(Error)
        }
        

Result 类型被用来封装异步操作生成的返回值和异常(同步操作可以使用 throw 来返回异常)。

比如说,一个发送异步 HTTP 请求的函数可能使用 Result 类型来存储响应或错误:

func fetch(_ request: Request, completion: (Result<Response, Error>) -> Void) {
        // ...
        }
        

调用这个方法后,你可以使用 switch 来分别处理它的 .success.failure

fetch(request) { result in
        switch result {
        case .success(let value):
        print("Success: \(value)")
        case .failure(let error):
        print("Failure: \(error)")
        }
        }
        

现在假设有一个函数会在它的 completion 中永远返回成功结果:

func alwaysSucceeds(_ completion: (Result<String, Never>) -> Void) {
        completion(.success("yes!"))
        }
        

ResultError 类型指定为 Never 后,我们可以使用类型检测体系来表明失败是永远不可能发生的。这样做的好处在于,你不需要处理 .failure,Swift 可以推断出这个 switch 语句已经处理了所有情况。

alwaysSucceeds { (result) in
        switch result {
        case .success(let string):
        print(string)
        }
        }
        

下面这个例子是让 Never 遵循 Comparable 协议,这段代码把 Never 用到了极致:

extension Never: Comparable {
        public static func < (lhs: Never, rhs: Never) -> Bool {
        switch (lhs, rhs) {}
        }
        }
        

因为 Never 是一个无实例类型,所以它没有任何可能的值。所以当我们使用 switch 遍历它的 lhsrhs 时,Swift 可以确定所有的可能性都遍历了。既然所有的可能性 — 实际上这里不存在任何值 — 都返回了 Bool,那么这个方法就可以正常编译。

工整!

使用 Never 作为兜底类型

实际上,关于 Never 的 Swift Evolution 提案中已经暗示了这个类型在未来可能有更多用处:

一个无实例类型可以作为其他任意类型的子类型 — 如果某个表达式根本不可能产生任何结果,那么我们就不需要关心这个表达式的类型到底是什么。如果编译器支持这一特性,就可以实现很多有用的功能……

解包或者死亡

强制解包操作(!)是 Swift 中最具争议的部分之一。(在代码中使用这个操作符)往好了说,是有意为之(在异常时故意让程序崩溃);往坏了说,可能表示使用者没有认真思考。在缺乏其他信息的情况下,很难看出这两者的区别。

比如,下面的代码假定数组一定不为空,

let array: [Int]
        let firstIem = array.first!
        

为了避免强制解包,你可以使用带条件赋值的 guard 语句:

let array: [Int]
        guard let firstItem = array.first else {
        fatalError("array cannot be empty")
        }
        

未来,如果 Never 成为兜底类型,它就可以用在 nil-coalescing operator 表达式的右边。

// 未来的 Swift 写法? 🔮
        let firstItem = array.first ?? fatalError("array cannot be empty")
        

如果你想现在就使用这种模式,可以手动重载 ?? 运算符(但是……):

func ?? <T>(lhs: T?, rhs: @autoclosure () -> Never) -> T {
        switch lhs {
        case let value?:
        return value
        case nil:
        rhs()
        }
        }
        

在拒绝 SE-0217: Introducing the !! “Unwrap or Die” operator to the Swift Standard Library原因说明中, Joe Groff 提到,“我们发现重载 [?? for Never] 会对类型检测的性能产生难以接受的影响”。所以,不建议你在自己的代码中添加上面的代码。

表达式风格的 Throw

类似的,如果 throw 可以从语句变成一个返回 Never的表达式,你就可以在 ?? 右边使用 throw

// 未来的 Swift 写法? 🔮
        let firstItem = array.first ?? throw Error.empty
        

带类型的 Throw

继续研究下去:如果函数声明的 throw 关键字支持类型约束,那么 Never 可以用来表明某个函数绝对不会抛出异常(类似于在上面的 Result 例子):

// 未来的 Swift 写法? 🔮
        func neverThrows() throws<Never> {
        // ...
        }
        neverThrows() // 无需使用 `try` ,因为编译器保证它一定成功(可能)
        

声称某个事情永远不可能发生,就像是向整个宇宙发出邀请,来证明它是错的一样。情态逻辑(modal logic)或者信念逻辑(doxastic logic)允许保面子式的妥协(“它当时是对的,至少我是这么认为的!”),但时态逻辑(temporal logic)似乎将这个约定提到了更高的一个标准。

幸运的是,得益于最不像类型的 Never,Swift 到达了这个高标准。

guard & defer

作者 Mattt
2015年10月5日 00:00

「我们应该(聪明的程序员明白自己的局限性)尽力……让文本里的程序(program)和时间轴上的进程(process)的对应尽量简单。」

Edsger W. Dijkstra, 《Go To 有害论》

很遗憾,他的文章通常只因为使《____有害论》这种文章标题在程序员中流行起来,还有网上对这些论文不妥当的抨击出现时,才会被想起。因为 Dijkstra(照常)提出了一个很好的观点:代码结构应该反映其行为。

Swift 2.0 带来了两个新的能够简化程序和提高效率的控制流表达形式:guarddefer。前者可以让代码编写更流畅,后者能够让执行推迟。

我们应该如何使用这两个新的声明方式呢?guarddefer 将如何帮我们厘清程序和进程间的对应关系呢?

我们 defer(推迟)一下 defer 先看 guard


guard

guard 是一个要求表达式的值为 true 从而继续执行的条件语句。如果表达式为 false,则会执行必须提供的 else 分支。

func sayHello(numberOfTimes: Int) {
        guard numberOfTimes > 0 else {
        return
        }
        for _ in 1...numberOfTimes {
        print("Hello!")
        }
        }
        

guard 语句中的 else 分支必须退出当前的区域,通过使用 return 来退出函数,continue 或者 break 来退出循环,或者使用像 fatalError(_:file:line:) 这种返回 Never 的函数。

guard 语句和 optional 绑定组合在一起非常好用。在 guard 语句的条件里进行的 optional 绑定可以在函数或闭包其后的部分使用。

对比一下 guard-let 语句和 if-let 语句中的 optional 绑定:

var name: String?
        if let name = name {
        // name 在这里面不是 optional(类型是 String)
        }
        // name 在外面是 optional(类型是 String?)
        guard let name = name else {
        return
        }
        // name 从这里开始都不是 optional 了(类型是 String)
        

如果说在 Swift 1.2 中介绍的并行 optional 绑定领导了对 厄运金字塔 的革命,那么 guard 声明则与之一并将金字塔摧毁。

for imageName in imageNamesList {
        guard let image = UIImage(named: imageName)
        else { continue }
        // do something with image
        }
        

使用 guard 来避免过多的缩进和错误

我们来对比一下使用 guard 关键字之后能如何改善代码且帮助我们避免错误。

比如,我们要实现一个 readBedtimeStory() 函数:

enum StoryError: Error {
        case missing
        case illegible
        case tooScary
        }
        func readBedtimeStory() throws {
        if let url = Bundle.main.url(forResource: "book",
        withExtension: "txt")
        {
        if let data = try? Data(contentsOf: url),
        let story = String(data: data, encoding: .utf8)
        {
        if story.contains("👹") {
        throw StoryError.tooScary
        } else {
        print("Once upon a time... \(story)")
        }
        } else {
        throw StoryError.illegible
        }
        } else {
        throw StoryError.missing
        }
        }
        

要读一个睡前故事,我们需要能找到一本书,这本故事书必须要是可读的,并且故事不能太吓人(请不要让怪物出现在书的结尾,谢谢你!)。

请注意 throw 语句离检查本身有多远。你需要读完整个方法来找到如果没有 book.txt 会发生什么。

像一本好书一样,代码应该讲述一个故事:有着易懂的情节,清晰的开端、发展和结尾。(请尝试不要写太多「后现代」风格的代码。)

使用 guard 语句组织代码可以让代码读起来更加的线性:

func readBedtimeStory() throws {
        guard let url = Bundle.main.url(forResource: "book",
        withExtension: "txt")
        else {
        throw StoryError.missing
        }
        guard let data = try? Data(contentsOf: url),
        let story = String(data: data, encoding: .utf8)
        else {
        throw StoryError.illegible
        }
        if story.contains("👹") {
        throw StoryError.tooScary
        }
        print("Once upon a time ...\(story)")
        }
        

这样就好多了! 每一个错误都在相应的检查之后立刻被抛出,所以我们可以按照左手边的代码顺序来梳理工作流的顺序。

不要在 guard 中双重否定

不要滥用这个新的流程控制机制——特别是在条件表达式已经表示否定的情况下。

举个例子,如果你想要在一个字符串为空是提早退出,不要这样写:

// 啊?
        guard !string.isEmpty else {
        return
        }
        

保持简单。自然的走下去。避免双重否定。

// 噢!
        if string.isEmtpy {
        return
        }
        

defer

在错误处理方面,guard 和新的 throw 语法之间,Swift 鼓励用尽早返回错误(这也是 NSHipster 最喜欢的方式)来代替嵌套 if 的处理方式。尽早返回让处理更清晰了,但是已经被初始化(可能也正在被使用)的资源必须在返回前被处理干净。

defer 关键字为此提供了安全又简单的处理方式:声明一个 block,当前代码执行的闭包退出时会执行该 block。

看看下面这个包装了系统调用 gethostname(2) 的函数,用来返回当前系统的主机名称

import Darwin
        func currentHostName() -> String {
        let capacity = Int(NI_MAXHOST)
        let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)
        guard gethostname(buffer, capacity) == 0 else {
        buffer.deallocate()
        return "localhost"
        }
        let hostname = String(cString: buffer)
        buffer.deallocate()
        return hostname
        }
        

这里有一个在最开始就创建的 UnsafeMutablePointer<UInt8> 用于存储目标数据,但是我既要在错误发生后销毁它,又要在正常流程下不再使用它时对其进行销毁。

这种设计很容易导致错误,而且不停地在做重复工作。

通过使用 defer 语句,我们可以排除潜在的错误并且简化代码:

func currentHostName() -> String {
        let capacity = Int(NI_MAXHOST)
        let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)
        defer { buffer.deallocate() }
        guard gethostname(buffer, capacity) == 0 else {
        return "localhost"
        }
        return String(cString: buffer)
        }
        

尽管 defer 紧接着出现在 allocate(capacity:) 调用之后,但它要等到当前区域结束时才会被执行。多亏了 deferbuffer 才能无论在哪个点退出函数都可以被释放。

考虑在任何需要配对调用的 API 上都使用 defer,比如 allocate(capacity:) / deallocate()wait() / signal()open() / close()。这样的话,你不仅可以消除一种程序员易犯的错误,还能让 Dijkstra 自豪地用它的母语德语说:「Goed gedaan!」。

经常 defer

如果在同一个作用域内使用多个 defer 语句,它们会根据出现顺序反过来执行——像栈一样。这个反序是非常重要的细节,保证了被延迟的代码块创建时作用域内存在的东西,在代码块执行同样存在。

举个例子,执行这段代码会得到下面的输出:

func procrastinate() {
        defer { print("wash the dishes") }
        defer { print("take out the recycling") }
        defer { print("clean the refrigerator") }
        print("play videogames")
        }
        

play videogames
clean the refrigerator
take out the recycling
wash the dishes

如果你像这样嵌套 defer 语句,会怎么样?

defer { defer { print("clean the gutter") } }
        

你的第一想法可能是语句会被压入栈的最底部。但并不是这样的。仔细想一想,然后在 Playground 里验证你的猜想。

正确 defer

如果在 defer 语句中引用了一个变量,执行时会用到变量最终的值。换句话说:defer 代码块不会捕获变量当前的值。

如果你运行这段代码,你会得到下面的输出:

func flipFlop() {
        var position = "It's pronounced /ɡɪf/"
        defer { print(position) }
        position = "It's pronounced /dʒɪf/"
        defer { print(position) }
        }
        

It's pronounced /dʒɪf/
It's pronounced /dʒɪf/

仔细 defer

另一件需要注意的事情,那就是 defer 代码块无法跳出它所在的作用域。因此如你尝试调用一个会 throw 的方法,抛出的错误就无法传递到其周围的上下文。

func burnAfterReading(file url: URL) throws {
        defer { try FileManager.default.removeItem(at: url) }
        // 🛑 Errors not handled
        let string = try String(contentsOf: url)
        }
        

作为替代,你可以使用 try? 来无视掉错误,或者直接将语句移出 defer 代码块,将其放到函数的最后,正常的执行。

(其他情况下)Defer 会带来坏处

虽然 defer 像一个语法糖一样,但也要小心使用避免形成容易误解、难以阅读的代码。在某些情况下你可能会尝试用 defer 来对某些值返回之前做最后一步的处理,例如说在后置运算符 ++ 的实现中:

postfix func ++(inout x: Int) -> Int {
        let current = x
        x += 1
        return current
        }
        

在这种情况下,可以用 defer 来进行一个很另类的操作。如果能在 defer 中处理的话为什么要创建临时变量呢?

postfix func ++(inout x: Int) -> Int {
        defer { x += 1 }
        return x
        }
        

这种写法确实聪明,但这样却颠倒了函数的逻辑顺序,极大降低了代码的可读性。应该严格遵循 defer 在整个程序最后运行以释放已申请资源的原则,其他任何使用方法都可能让代码乱成一团。


「聪明的程序员明白自己的局限性」,我们必须权衡每种语言特性的好处和其成本。

类似于 guard 的新特性能让代码流程上更线性,可读性更高,就应该尽可能使用。

同样 defer 也解决了重要的问题,但是会强迫我们一定要找到它声明的地方才能追踪到其销毁的方法,因为声明方法很容易被滚动出了视野之外,所以应该尽可能遵循它出现的初衷尽可能少地使用,避免造成混淆和晦涩。

❌
❌