做者:Mattt,原文连接,原文日期:2018-10-31 译者:zhongWJ;校对:numbbbbb,pmst;定稿:Forelaxgit
从 咱们第一篇关于 Objective-C 中的 nil
的文章 到 最近对 Swift 中 Never
类型的一瞥,“不存在”一直是 NSHipster 讨论的话题。但今天的文章多是它们当中充斥着最多如 恐怖留白 般细节的 —— 由于咱们将目光聚焦在了 Swift 中的 Void
上。github
Void
是什么?在 Swift 中,它只不过是一个空元组。编程
typealias Void = ()
复制代码
咱们使用 Void
时才会开始关注它。swift
let void: Void = ()
void. // 没有代码补全提示
复制代码
Void
类型的值没有成员:既没有成员方法,也没有成员变量,甚至连名字都没有。它并不比 nil
多些什么。对于一个空容器,Xcode 不会给咱们任何代码补全提示。服务器
在标准库中,Void
类型最显著和奇特的用法是在 ExpressibleByNilLiteral
协议中。网络
protocol ExpressibleByNilLiteral {
init(nilLiteral: ())
}
复制代码
听从 ExpressibleByNilLiteral
协议的类型能够用 nil
字面量来初始化。大多数类型并不听从这个协议,由于用 Optional
来表示值可能不存在会更容易理解。但偶尔你也会碰到 ExpressibleByNilLiteral
。闭包
ExpressibleByNilLiteral
的指定构造方法不接收任何实际参数。(假设接收了,那结果会怎么样?)然而,该协议的指定构造方法不能仅仅只是一个空构造方法 init()
,由于不少类型用它做为默认构造方法。app
你能够将指定构造方法改成一个返回 nil
的类型方法(Type Method)来尝试解决这个问题,但一些强制内部可见的状态在构造方法外就不能使用了。在这里咱们使用一种更好的解决方案,给构造方法增长一个带 Void
参数的 nilLiteral
标签。这巧妙的利用已有的功能来实现很是规的结果。dom
元组以及元类型(例如 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?
,时间足够长,你可能会将它和 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
类型。
当读到这么使人发寒的描述时,若是你开始打寒颤了,你能够引导 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
类型的文章,你应该知道,将 Result
的 Error
类型设为 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。