作者:Mattt, 原文链接 ,原文日期:2018-10-31 译者: zhongWJ ;校对: numbbbbb , pmst ;定稿: 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.Type
,
Int.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
),这意味着函数没有出错直接返回。如果
success
是
nil
,那我们就知道函数生成了一个错误。
let success: Void? = try? failsRandomly()
if success != nil {
// 成功执行
} else {
// 失败执行
}
复制代码
很多人可能不喜欢
do / catch
代码块,但你不得不承认,相比这里的代码,
do / catch
代码块更加优雅。
在某些特殊场景下,这种变通方式可能会很有用。例如为了保存每一次自评估闭包执行的副作用,你可以在类上使用静态属性:
static var oneTimeSideEffect: Void? = {
return try? data.write(to: fileURL)
}()
复制代码
虽然这样可行,但更好的办法是使用
Error
和
Bool
类型。
夜晚才会响("Clang")的东西
当读到这么令人发寒的描述时,如果你开始打寒颤了,你可以引导
Void
类型的坏死能量来召唤巨大的热量给自己的精神加热: