Never

SwiftGG翻譯組發表於2019-03-01

原文連結:swift.gg/2018/08/30/…
作者:Matt
譯者:雨謹
校對:numbbbbb,wongzigii,Firecrest
定稿:CMB

Never 是一個約定,表示一件事在過去或未來的任何時段都不會發生。它是時間軸上的一種邏輯上的不可能,在任何方向延展開去都沒有可能。這就是為什麼在程式碼中看到 這樣的註釋 會特別讓人不安。

// 這個不會發生
複製程式碼

所有編譯器的教科書都會告訴你,這樣一句註釋不能也不會對編譯出的程式碼產生任何影響。墨菲定理 告訴你並非如此,註釋以下的程式碼一定會被觸發。

那 Swift 是如何在這種無法預測的、混亂的開發過程中保證安全呢?答案難以置信:“什麼都不做”,以及“崩潰”。


使用 Never 替換 @noreturn 修飾符,是由 Joe GroffSE-0102: “Remove @noreturn attribute and introduce an empty Never type” 中提出的。

在 Swift 3 之前,那些要中斷執行的函式,比如 fatalError(_:file:line:)abort()exit(_:),需要使用 @noreturn 修飾符來宣告,這會告訴編譯器,執行完成後不用返回撥用這個函式的位置。

// Swift < 3.0
@noreturn func fatalError(_ message: () -> String = String(),
                               file: StaticString = #file,
                               line: UInt = #line)
複製程式碼

從 Swift 3 開始,fatalError 和它的相關函式都被宣告為返回 Never 型別。

// Swift >= 3.0
func fatalError(_ message: @autoclosure () -> String = String(),
                     file: StaticString = #file,
                     line: UInt = #line) -> Never
複製程式碼

作為一個註釋的替代品,它肯定是很複雜的,對嗎?NO!事實上,恰恰相反,Never 可以說是整個 Swift 標準庫中最簡單的一個型別:

enum Never {}
複製程式碼

無例項型別(Uninhabited Types

Never 是一個_無例項_(Uninhabited)型別,也就是說它沒有任何值。或者換句話說,無例項型別是無法被構建的。

在 Swift 中,沒有定義任何 case 的列舉是最常見的一種無例項型別。跟結構體和類不同,列舉沒有初始化方法。跟協議也不同,列舉是一個具體的型別,可以包含屬性、方法、泛型約束和巢狀型別。正因如此,Swift 標準庫廣泛使用無例項的列舉型別來做諸如 定義名稱空間 以及 標識型別的含義 之類的事情。

Never 並不這樣。它沒有什麼花哨的東西,它的特別之處就在於,它就是它自己(或者說,它什麼都不是)。

試想一個返回值為無例項型別的函式:因為無例項型別沒有任何值,所以這個函式無法正常的返回。(它要如何生成這個返回值呢?)所以,這個函式要麼停止執行,要麼無休止的一直執行下去。

消除泛型中的不可能狀態

從理論角度上說,Never 確實很有意思,但它在實際應用中又能幫我們做什麼呢?

做不了什麼,或者說在 SE-0215: Conform Never to Equatable and Hashable 推出以前,做不了什麼。

Matt Diephouse 在提案中解釋了為什麼讓這個令人費解的型別去遵守 Equatable 和其他協議:

Never 在表示不可能執行的程式碼方面非常有用。大部分人熟悉它,是因為它是 fatalError 等方法的返回值,但 Never 在泛型方面也非常有用。比如說,一個 Result 型別可能使用 Never 作為它的 Value,表示某種東西一直是錯誤的,或者使用 Never 作為它的 Error,表示某種東西一直不是錯誤的。

Swift 沒有標準的 Result 型別,大部分情況下它們是這個樣子的:

enum Result<Value, Error: Swift.Error> {
    case success(Value)
    case failure(Error)
}
複製程式碼

Result 型別被用來封裝非同步操作生成的返回值和異常(同步操作可以使用 throw 來返回異常)。

比如說,一個傳送非同步 HTTP 請求的函式可能使用 Result 型別來儲存響應或錯誤:

func fetch(_ request: Request, completion: (Result<Response, Error>) -> Void) {
    // ...
}
複製程式碼

呼叫這個方法後,你可以使用 switch 來分別處理它的 .success.failure

fetch(request) { result in
    switch result {
    case .success(let value):
        print("Success: (value)")
    case .failure(let error):
        print("Failure: (error)")
    }
}
複製程式碼

現在假設有一個函式會在它的 completion 中永遠返回成功結果:

func alwaysSucceeds(_ completion: (Result<String, Never>) -> Void) {
    completion(.success("yes!"))
}
複製程式碼

ResultError 型別指定為 Never 後,我們可以使用型別檢測體系來表明失敗是永遠不可能發生的。這樣做的好處在於,你不需要處理 .failure,Swift 可以推斷出這個 switch 語句已經處理了所有情況。

alwaysSucceeds { (result) in
    switch result {
    case .success(let string):
        print(string)
    }
}
複製程式碼

下面這個例子是讓 Never 遵循 Comparable 協議,這段程式碼把 Never 用到了極致:

extension Never: Comparable {
  public static func < (lhs: Never, rhs: Never) -> Bool {
    switch (lhs, rhs) {}
  }
}
複製程式碼

因為 Never 是一個無例項型別,所以它沒有任何可能的值。所以當我們使用 switch 遍歷它的 lhsrhs 時,Swift 可以確定所有的可能性都遍歷了。既然所有的可能性 — 實際上這裡不存在任何值 — 都返回了 Bool,那麼這個方法就可以正常編譯。

工整!

使用 Never 作為兜底型別

實際上,關於 Never 的 Swift Evolution 提案中已經暗示了這個型別在未來可能有更多用處:

一個無例項型別可以作為其他任意型別的子型別 — 如果某個表示式根本不可能產生任何結果,那麼我們就不需要關心這個表示式的型別到底是什麼。如果編譯器支援這一特性,就可以實現很多有用的功能……

解包或者死亡

強制解包操作(!)是 Swift 中最具爭議的部分之一。(在程式碼中使用這個操作符)往好了說,是有意為之(在異常時故意讓程式崩潰);往壞了說,可能表示使用者沒有認真思考。在缺乏其他資訊的情況下,很難看出這兩者的區別。

比如,下面的程式碼假定陣列一定不為空,

let array: [Int]
let firstIem = array.first!
複製程式碼

為了避免強制解包,你可以使用帶條件賦值的 guard 語句:

let array: [Int]
guard let firstItem = array.first else {
    fatalError("array cannot be empty")
}
複製程式碼

未來,如果 Never 成為兜底型別,它就可以用在 nil-coalescing operator 表示式的右邊。

// 未來的 Swift 寫法? ?
let firstItem = array.first ?? fatalError("array cannot be empty")
複製程式碼

如果你想現在就使用這種模式,可以手動過載 ?? 運算子(但是……):

func ?? <T>(lhs: T?, rhs: @autoclosure () -> Never) -> T {
    switch lhs {
    case let value?:
        return value
    case nil:
        rhs()
    }
}
複製程式碼

在拒絕 SE-0217: Introducing the !! “Unwrap or Die” operator to the Swift Standard Library原因說明中, Joe Groff 提到,“我們發現過載 [?? for Never] 會對型別檢測的效能產生難以接受的影響”。所以,不建議你在自己的程式碼中新增上面的程式碼。

表示式風格的 Throw

類似的,如果 throw 可以從語句變成一個返回 Never的表示式,你就可以在 ?? 右邊使用 throw

// 未來的 Swift 寫法? ?
let firstItem = array.first ?? throw Error.empty
複製程式碼

帶型別的 Throw

繼續研究下去:如果函式宣告的 throw 關鍵字支援型別約束,那麼 Never 可以用來表明某個函式絕對不會丟擲異常(類似於在上面的 Result 例子):

// 未來的 Swift 寫法? ?
func neverThrows() throws<Never> {
    // ...
}

neverThrows() // 無需使用 `try` ,因為編譯器保證它一定成功(可能)
複製程式碼

聲稱某個事情永遠不可能發生,就像是向整個宇宙發出邀請,來證明它是錯的一樣。情態邏輯(modal logic)或者信念邏輯(doxastic logic)允許保面子式的妥協(“它當時是對的,或者我是這麼認為的!”),但時態邏輯(temporal logic)似乎將這個約定提到了更高的一個標準。

幸運的是,得益於最不像型別的 Never,Swift 到達了這個高標準。