原文: Optionals and String Interpolation
作者: Ole Begemann
譯者: kemchenj
你知道這個問題嗎? 你想要在 UI 上顯示一個 Optional 值, 或者是在控制臺打印出來, 但你不喜歡默認的 Optional 字符串的顯示方式: "Optional(...)" 或者是 "nil". 例如:
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 的討論:
由于 Optional 值永遠都不應該顯示給終端用戶, 而它又經常作為一個控制臺里的驚喜存在, 我們覺得獲取一個 Optional 值的 debug 信息是一種"明確"的藝術. 提案目前的主要內容是, 在一個字符串片段里使用 Optional 值的時候需要發出一個警告.
在最新的 Swift 開發版本(2016-12-01)里已經實現了這個警告:
你有幾個方法可以去掉這個警告:
- 添加一個顯式轉換, 例如
someValue as Int?
- 使用
String(describing: someValue)
函數 - 提供一個默認值去讓表達式不為 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 團隊后來也沒提到過對于這樣的做法有怎樣的優化, 所以我估計還是會污染全局的
如果有了解的人, 或者已經做過測試的人, 可以的話告訴一下我準確的結果