使用 @autoclosure 來設計 Swift API#
此文是翻譯#
原文連結:Using @autoclosure when designing Swift APIs
Swift @autoclosure 屬性用於在閉包中定義一個 “被包裹” 的參數。主要用於延遲執行一段(潛在的耗時、占資源大)代碼到真正需要的時候,而不是在參數傳遞時就執行。
在 Swift 標準庫中就有一個例子,Assert 函數的使用。由於 asserts 僅僅在 debug 模式下觸發,所以沒必要在 release 模式下執行代碼。這時就用到了 @autoclosure:
func assert(_ expression: @autoclosure() -> Bool,
_ message: @autoclosure() -> String) {
guard isDebug else {
return
}
// Inside assert we can refer to expression as a normal closure
if !expression() {
assertionFailure(message())
}
}
上面是我理解的 assert 的實現,assert 的真正實現在這裡
@autoclosure 的一個好處是對於調用位置沒有影響。如果 assert 是用 “一般” 閉包的方式實現的,那它的使用是這樣:
assert({ someConfition() }, { "Hey, it failed!" })
但是現在,它可以被當作函數接收無閉包參數來使用:
assert(someCondition(), "Hey it failed!")
這週,我們來看一下怎麼在我們自己的代碼中使用 @autoclosure,以及如何使用它設計出優雅的 API。
內聯函數#
@autoclosure 的一個作用是在函數調用中內聯表達式。這使得我們可以把指定的表達式做為一個參數來使用。我們來看一下下面的例子:
在 iOS 中,通常使用下面的 API 來實現 view 動畫:
UIView.animate(withDuration: 0.25) {
view.frame.origin.y = 100
}
使用 @autoclosure,我們可以寫一個動畫函數,自己創建動畫的閉包,並且執行。像下面這樣:
func animate(_ animation: @autoclosure @escaping () -> Void,
duration: TimeInterval = 0.25) {
UIView.animate(withDuration: duration, animations: animation)
}
然後,我們就可以僅僅調用一個簡單的函數,沒有多餘的 {},來調用動畫:
animate(view.frame.origin.y = 100)
通過上面的方法,我們減少了冗長的動畫代碼,且沒有喪失可讀性。
將錯誤作為表達式傳遞#
@autoclosure 的另一個十分有用的地方是,在寫處理錯誤的工具類的時。例如,我們想要通過 throwing API,給 Optional 添加擴展,來解包它。這種情況下,我們要 Optional 為非空,或者就 throw 錯誤,如下:
extension Optional {
func unwrapThrow(_ errorExpression: @autoclosure() -> Error) {
guard let value = self else {
throw errorExpression()
}
return value
}
}
和 assert 的實現類似,這樣我們判斷錯誤表達式只在需要的時候,而不是每次解包 Optional時都判空。我們現在可以如下使用 unwrapOrThrow API:
let name = try argument(at: 1).unwrapOrThrow(ArgumentError.missingName)
使用默認值進行類型推斷#
@autoclosure 最後一個使用場景是,從 dictionary、database、UserDefaults 中提取一個可空的值時。
一般情況下,從一個不確定類型的 dictionary 中獲取一個屬性,並且提供一個默認值,需要寫如下代碼:
let coins = (dictionay["numberOfCoins"] as? Int) ?? 100
上面的代碼不僅難於閱讀,而且還有類型推斷和??運算符。我們可以使用 @autoclosure,定義一個 API,來實現同樣的表達:
let coins = dictionary.value(forKey: "numberOfCoins", defaultValue: 100)
如上,我們可以看出,defaultValue 一方面作為默認值用於值缺失時,另一方面也用於類型推斷,不需要特別聲明類型或者類型捕捉。簡潔明了👍
然後我們來一下怎麼樣定義一個這樣的 API:
extension Dictionary where Value = Any {
func value<T>(forKey key: Key, defaultValue: @autoclosure () -> T) -> T {
guard let value = self[key] as? T else {
return defaultValue()
}
return value
}
}
同樣的,我們使用 @autoclosure 避免了每次執行方法的時候都判斷 defaultValue。
結論#
減少冗長代碼是需要仔細考慮的。我們的目標是寫出能自我解釋的、易於閱讀的代碼,所以在設計低冗余 API 時,我們需要確定,沒有在調用中移除重要信息。
我認為恰當使用 @autoclosure,是實現上述目的的偉大工具。處理表達式、不僅僅是值,不僅可以減少冗長的代碼,也可以潛在的提高性能。
你認為 @autoclosure 還有其他使用情景嗎?或者有問題、評論、反饋,請聯繫我。Twitter: @johnsundell.
感謝閱讀!🚀