阅读 498

Void

作者:Mattt,原文链接,原文日期:2018-10-31 译者:zhongWJ;校对:numbbbbbpmst;定稿:Forelax

我们第一篇关于 Objective-C 中的 nil 的文章最近对 Swift 中 Never 类型的一瞥,“不存在”一直是 NSHipster 讨论的话题。但今天的文章可能是它们当中充斥着最多如 恐怖留白 般细节的 —— 因为我们将目光聚焦在了 Swift 中的 Void 上。

Void 是什么?在 Swift 中,它只不过是一个空元组。

typealias Void = ()
复制代码

我们使用 Void 时才会开始关注它。

let void: Void = ()
void. // 没有代码补全提示
复制代码

Void 类型的值没有成员:既没有成员方法,也没有成员变量,甚至连名字都没有。它并不比 nil 多些什么。对于一个空容器,Xcode 不会给我们任何代码补全提示。

为“不存在”而生之物

在标准库中,Void 类型最显著和奇特的用法是在 ExpressibleByNilLiteral 协议中。

protocol ExpressibleByNilLiteral {
    init(nilLiteral: ())
}
复制代码

遵从 ExpressibleByNilLiteral 协议的类型可以用 nil 字面量来初始化。大多数类型并不遵从这个协议,因为用 Optional 来表示值可能不存在会更容易理解。但偶尔你也会碰到 ExpressibleByNilLiteral

ExpressibleByNilLiteral 的指定构造方法不接收任何实际参数。(假设接收了,那结果会怎么样?)然而,该协议的指定构造方法不能仅仅只是一个空构造方法 init(),因为很多类型用它作为默认构造方法。

你可以将指定构造方法改为一个返回 nil 的类型方法(Type Method)来尝试解决这个问题,但一些强制内部可见的状态在构造方法外就不能使用了。在这里我们使用一种更好的解决方案,给构造方法增加一个带 Void 参数的 nilLiteral 标签。这巧妙的利用已有的功能来实现非常规的结果。

如何比较“不存在”之物

元组以及元类型(例如 Int.TypeInt.self 返回结果),函数类型(例如 (String) -> Bool),existential 类型(例如 Encodable & Decodable)组成了非正式类型。与包含 swift 大部分的正式类型或命名类型不同,非正式类型是相对其他类型来定义的。

非正式类型不能被扩展。Void 是一个空元组,而由于元组是非正式类型,所以你不能给 Void 添加方法、属性或者遵从协议。

extension Void {} // 非正式类型 `Void` 不能被扩展
复制代码

Void 不遵从 Equatable协议,因为它不能这么做。然而当我们调用等于操作符(==)时,它如我们期望的一样运行正确。

void == void // true
复制代码

下面这个全局函数定义在所有正式协议之外,它实现了这个看似矛盾的行为。

func == (lhs: (), rhs: ()) -> Bool {
    return true
}
复制代码

小于操作符(<)也被同样处理,用这种方式来替代 Comparable 协议及其衍生出的其他比较操作符。

func < (lhs: (), rhs: ()) -> Bool {
    return false
}
复制代码

Swift 标准库为大小最多为 6 的元组提供了比较函数的实现。然而这是一种 hack 方式。Swift 核心团队在许多时候都显露过想要给元组增加对 Equatable 协议的支持的兴趣,但在实现的时候,并没有讨论过正式的提议。

壳中之鬼

作为非正式类型,Void 不能被扩展。但 Void 毕竟是一个类型,所以能被当作泛型约束来使用。

例如,考虑以下单个值的泛型容器:

struct Wrapper<Value> {
    let value: Value
}
复制代码

当泛型容器所包装的值的类型本身遵循 Equatable 协议时,利用 Swift 4.1 的杀手锏特性 条件遵循,我们首先可以扩展 Wrapper 让其支持 Equatable 协议。

extension Wrapper: Equatable where Value: Equatable {
    static func ==(lhs: Wrapper<Value>, rhs: Wrapper<Value>) -> Bool {
        return lhs.value == rhs.value
    }
}
复制代码

利用同之前一样的技巧,我们可以实现一个接受 Wrapper<Void> 参数的 == 全局函数,来达到和 Equatable 协议几乎一样的效果。

func ==(lhs: Wrapper<Void>, rhs: Wrapper<Void>) -> Bool {
    return true
}
复制代码

在这种情况下,我们就可以比较两个包装了 Void 值的 Wrapper

Wrapper(value: void) == Wrapper(value: void) // true
复制代码

然而,当我们尝试将这样一个包装值赋值给一个变量时,编译器会生成诡异的错误。

let wrapperOfVoid = Wrapper<Void>(value: void)
// 👻 错误: 不能赋值:
// 由于找不到对应符号,无法销毁 wrapperOfVoid
复制代码

Void 的可怕之处反过来再次自我否定。

幽灵类型

即使你不敢提及它的非正式名字,你依然逃不过 Void 的掌心。

任何没有显式声明返回值的函数会隐式的返回一个 Void

func doSomething() { ... }

// 等同于

func doSomething() -> Void { ... }
复制代码

这个行为很奇怪,但不是特别有用。并且当你将一个返回 Void 类型的函数的返回值赋值给一个变量时,编译器会生成一个警告。

doSomething() // 没有警告

let result = doSomething()
// ⚠️ 常量 `result` 指向的是一个 `Void` 类型的值,这种行为的结果不可预测
复制代码

你可以显式指定变量类型为 Void 来消除警告。

let result: Void = doSomething() // ()
复制代码

相反的,当函数的返回值类型为非 Void 时,你如果不将返回值赋值给其他变量,编译器也会产生警告。更多详情可以参考 SE-0047 “默认当非 Void 函数返回结果未使用时告警”

试着从 Void 恢复过来

如果你斜视 Void?,时间足够长,你可能会将它和 Bool 弄混。这两种类型类似,都仅有两种状态:true / .some(()) 以及 false / .none

但类似并不意味着一样。它们两最明显的不同是,Bool 遵循 ExpressibleByBooleanLiteral 协议,而 Void 不是也不能遵循 ExpressibleByBooleanLiteral 协议,和它不能遵循 Equatable 协议的原因一样。所以你不能这样做:

(true as Void?) // 错误
复制代码

Void 可能是 Swift 中最令人毛骨悚的类型了。但是当给 Bool 起一个 Booooooool 别名时, 就和 Void 不相上下了。

Void? 硬坳的话是能够表现的像 Bool 一样。比如下面这个随机抛出错误的函数:

struct Failure: Error {}

func failsRandomly() throws {
    if Bool.random() {
        throw Failure()
    }
}
复制代码

正确方式是,在一个 do / catch 代码块中用 try 表达式来调用这个函数。

do {
    try failsRandomly()
    // 成功执行
} catch {
    // 失败执行
}
复制代码

failsRandomly() 隐式返回 Void,利用这一事实可以达到同样效果,虽然不正确但表面上可行。try? 表达式会处理可能抛出异常的语句,将结果包装为一个可选类型值。对于 failsRandomly() 这种情况而言,结果是 Void?。假如 Void?.some 值(即,!= nil),这意味着函数没有出错直接返回。如果 successnil,那我们就知道函数生成了一个错误。

let success: Void? = try? failsRandomly()
if success != nil {
    // 成功执行
} else {
    // 失败执行
}
复制代码

很多人可能不喜欢 do / catch 代码块,但你不得不承认,相比这里的代码,do / catch 代码块更加优雅。

在某些特殊场景下,这种变通方式可能会很有用。例如为了保存每一次自评估闭包执行的副作用,你可以在类上使用静态属性:

static var oneTimeSideEffect: Void? = {
   return try? data.write(to: fileURL)
}()
复制代码

虽然这样可行,但更好的办法是使用 ErrorBool 类型。

夜晚才会响("Clang")的东西

当读到这么令人发寒的描述时,如果你开始打寒颤了,你可以引导 Void 类型的坏死能量来召唤巨大的热量给自己的精神加热:

也就是说,通过以下代码让 lldb-rpc-server 全力开启 CPU(译者注:编译器会卡死):

extension Optional: ExpressibleByBooleanLiteral where Wrapped == Void {
    public typealias BooleanLiteralType = Bool

    public init(booleanLiteral value: Bool) {
        if value {
            self.init(())!
        } else {
            self.init(nilLiteral: ())!
        }
    }
}

let pseudoBool: Void? = true // 我们永远都不会发现是这里导致的
复制代码

按照洛夫克拉夫特式恐怖小说的传统,Void 有一个计算机无法处理的物理结构;我们简单地见证了它如何使一个进程无可救药的疯狂。

徒有其表的胜利

我们用一段熟悉的代码来结束这段神奇的学习之旅:

enum Result<Value, Error> {
    case success(Value)
    case failure(Error)
}
复制代码

如果你还记得之前 我们关于 Never 类型的文章,你应该知道,将 ResultError 类型设为 Never 可以让它表示某些总会成功的操作。

类似的,操作成功但不会生成有意义的结果,用 Void 作为 Value 类型可以表示。

例如,应用可能会通过简单的网络请求定时“ping”服务器来实现一个 心跳

func ping(_ url: URL, completion: (Result<Void, Error>) -> Void) {
    // ...
}
复制代码

根据 HTTP 语义,一个虚拟 /ping 终端正确的状态码应该是 204 No Content

在请求的回调中,通过下面的调用来表示成功:

completion(.success(()))
复制代码

假如你觉得括号太多了(其实又有什么问题呢?),给 Result 加一个关键的扩展可以让事情更简单点:

extension Result where Value == Void {
    static var success: Result {
        return .success(())
    }
}
复制代码

有付出就有收获。

completion(.success)
复制代码

虽然这看起来像一次纯理论甚至抽象的练习,但对 Void 的探究能让我们对 Swift 这门编程语言的基础有一个更深刻的认知。

在 Swift 还没有面世很久之前,元组在编程语言中扮演着重要角色。它们可以表示参数列表和枚举关联值,依场景不同而扮演不同角色。但在某些情况下,这个模型崩溃了。编程语言依然没有调和好这些不同结构之间的差异。

依据 Swift 神话,Void 将会是那些老神(译者注:旧的编程语言)的典范:它是一个真正的单例,你压根一丁点儿都不会注意到它的作用和影响;编译器也会忽略它。

可能这一切都只是我们理解力的边缘发明,是我们对这门语言前景担忧的一种表现。总之,当你凝视 Void 时,Void 也在凝视着你。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 swift.gg

关注下面的标签,发现更多相似文章
评论