做者: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。