Void

做者:Mattt,原文連接,原文日期:2018-10-31 譯者:zhongWJ;校對:numbbbbbpmst;定稿: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.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

相關文章
相關標籤/搜索