Optional 與 字符串 的交互

原文: Optionals and String Interpolation
做者: Ole Begemann
譯者: kemchenjhtml

你知道這個問題嗎? 你想要在 UI 上顯示一個 Optional 值, 或者是在控制檯打印出來, 但你不喜歡默認的 Optional 字符串的顯示方式: "Optional(...)" 或者是 "nil". 例如:git

var someValue: Int? = 5
print("這個值是 \(someValue)")
// "這個值是 Optional(5)"

someValue = nil
print("這個值是 \(someValue)")
// "這個值是 nil"

在字符串裏插入 Optional 值會有一些不可預料的結果

Swift 3.1 會在你往字符串裏插入一個 Optional 值的時候發出一個警告, 由於這個行爲可能會產生意料以外的結果. 這裏有 Julio Carrettoni, Harlan Haskins 和 Robert Widmann 在 Swift-Evolution 的討論:github

因爲 Optional 值永遠都不該該顯示給終端用戶, 而它又常常做爲一個控制檯裏的驚喜存在, 咱們以爲獲取一個 Optional 值的 debug 信息是一種"明確"的藝術. 提案目前的主要內容是, 在一個字符串片斷裏使用 Optional 值的時候須要發出一個警告.swift

在最新的 Swift 開發版本(2016-12-01)裏已經實現了這個警告:c#

圖片描述

你有幾個方法能夠去掉這個警告:api

  1. 添加一個顯式轉換, 例如 someValue as Int?安全

  2. 使用 String(describing: someValue)函數app

  3. 提供一個默認值去讓表達式不爲 Optional, 例如 someValue ?? defaultValue(一種解包形式)框架

上面的方式我都不是特別喜歡, 但這是編譯器能提供的最好的方式了. 第三種作法的問題是解包操做符 ?? 須要符合相應的類型 - 若是 ?? 左邊的類型是 T?的話, 那右邊的類型就必須是 T. 用上面的例子來描述的話, 就意味着我只可以提供一個 Int 來做爲默認值, 而不能是一個字符串, 在這種狀況下就達不到我想要的效果.函數

一個自定義的字符串解包操做符

我經過自定義一個字符串解包操做符來解決這個問題. 由於它來源於 ??, 因此我決定把它命名爲 ???. ???操做符的左邊是 Optional 值, 而在右邊就是這個 Optional 值的字符串默認值, 返回一個字符串. 若是這個 Optional 值是 non-nil 的, 那麼它就會解包而後返回這個值的字符串描述, 不然就會返回一個默認值, 下面是具體的實現:

infix operator ???: NilCoalescingPrecedence

public func ???<T>(optional: T?, defaultValue: @autoclosure () -> String) -> String {
    switch optional {
    case let value?: return String(describing: value)
    case nil: return defaultValue()
    }
}

@autoclosure 結構保證了右邊的值只會在須要的時候纔會被計算出來, 例如 Optional 值是 nil 的時候. 這就可讓你傳遞一個複雜的或者耗時的運算表達式進去, 而只會在特定狀況下才會影響到性能. 我不認爲這種狀況(表達式很複雜)會常常發生, 但它是參考了 ?? 操做符在標準庫裏的實現.(儘管我決定去掉標準庫實現裏的 throws/rethrows)

或者, 你能夠經過 Optional.map 只用一行代碼來實現這個操做符, 就像這樣:

public func ???<T>(optional: T?, defaultValue: @autoclosure () -> String) -> String {
    return optional.map { String(describing: $0) } ?? defaultValue()
}

這跟第一個實現的效果如出一轍, 用哪個只看你我的的口味和代碼習慣. 我不認爲哪個比另外一個更加清晰.

最後一件我想說的是, 你必須從 String(describing:) (更偏向於值的描述) 或者是 String(reflecting:) (更偏向於 debug 信息) 中作出一個選擇, 去轉化這個值. 前一個選擇更適合 UI 展現, 然後一個則更適合運行日誌. 甚至你能夠再自定義一個操做符 (例如: ????), 去適應平常 debug 需求.

實際使用

咱們使用 ??? 操做符來重構一下文章最開始的那個例子:

var someValue: Int? = 5
print("值是 \(someValue ??? "unknown")")
// "值是 5"

someValue = nil
print("值是 \(someValue ??? "unknown")")
// "值是 unknown"

這是一個很小的改變, 但我很喜歡

[1]. 我最開始其實以爲重載 ?? 就行了. 我喜歡這種方式是由於個人視線更加符合解包符號的含義, 但這也會在某些狀況下失去了類型安全的優勢, 由於老是會被編譯成 someOptional ?? "someValue" 的形式

譯者注

我想特別說明一點是, 在咱們的項目裏, 重載 ?? 或者是自定義操做符 ??? 其實是不會影響到咱們引入的庫的, 咱們定義的 ????? 都是默認 internal 的, 也就是說做用域只在 Module 內, 怎麼用都是沒問題的. 固然, 若是是多人協做的狀況就要權衡溝通成本和實際帶來便捷了.

若是是咱們本身想寫框架的話, 聲明爲 internal, 而後就放心大膽的用吧, 不會污染到外部做用域的

但我不太肯定聲明爲 public 的話會發生什麼事情, 根據我在 medium 上看到的這篇文章, 至少在 Swift 1.0 的時候, 這麼作是真的會污染全局的, Swift 團隊後來也沒提到過對於這樣的作法有怎樣的優化, 因此我估計仍是會污染全局的

若是有了解的人, 或者已經作過測試的人, 能夠的話告訴一下我準確的結果

相關閱讀

emptiness
將可選類型轉換爲錯誤拋出

相關文章
相關標籤/搜索