Frank(wchuang)

I'm Frank from Taiwan, an Sr. iOS Engineer @ByteDance in Shanghai.

如何使用 @autoclosure 設計 Swift APIs

02 Mar 2019 » swift, ios

Swift 中的 @autoclosure 屬性,可以讓你定義一個 可自動取得一個閉包 (closure) 內的包裹 (wrapped) 的參數。它主要用於延緩執行時間,而不是當這個參數傳入的時候就直接使用,因為執行的動作可能有點耗時。這麼說有點繞口,下面是一個官方 Swift 標準函式庫的 assert 例子,我們來看看 Apple 是怎麼實現這個方法的。

由於 assert 只有在 debug 模式下才會被觸發,在 release 模式下會被返回而不會執行 expression,完整的實作請看源碼: Assert.swift

func assert(_ expression: @autoclosure () -> Bool, 
			_ message: @autoclosure () -> String) {
	guard let isDebug else { return }

	if !expression() {
		assertionFailure(message())
	}
}

如果 assert 的實作方式是用一般的閉包實現,那你會這樣呼叫:

assert({ someCondition() }, { "hey, it failed." })

現在你只需要這樣即可,不用在傳入一個閉包參照。

assert(someCondition(), "hey, it failed.")

那麼如何運用 @autoclosure 這個方便的特性在實際 APIs 設計上面呢?

###1. Inlining assignments

在方法呼叫的時候,把一個表達式當做一個參數傳入。一般在 iOS 處理畫面動畫你會用到:

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)

2. Passing errors as expressions

我們常常會需要寫一個工具來處理 errors 的各種情況,此時使用 @autoclosure 就變得非常方便。 例如:我們寫一個 extension 來擴展 Optional,讓我們可以容易的解包 (unwrap) 或拋出異常 (throwing API)。

extension Optional {
	func unwrapOrThrow(_ errorExpression: @autoclosure () -> Error) throws -> Wrapped {
		guard let value = self else {
			throw errorExpression()
		}
		return value
	}
}

這跟上述的 assert 一樣,我們只需要關注在錯誤的表達式上面,而不需要每次都去判斷是否解包成功 (unwarp)。 我們現在可以這樣使用 unwarpOrThrow API:

let name = try argument(at: 1).unwarpOrThrow(ArgumentError.missingName)

3. Type inference using default values

很多時候當我們從 Dictionary, TextField.text, UserDefaults 或者其他地方取值的時候,常常會碰到可選值 (optional value) 的情況。

如:

let coins = (dictionary["numberOfCoins"] as? Int) ?? 100

這樣的寫法用了很多操作符號,程式碼變得難以閱讀。 這時候可以用 @aotuclosure 來改善寫法。

let coins = dictioanry.value(forKey: "numberOfCoins", defaultValue: 100)

這邊賦予了一個預設值給 coins,當內容為 nil 的情況。這樣是不是好閱讀多了,也不需要每次都去判斷是否為 nil。:grin: 不過我們還需要一個方法擴展 Dictionary。

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 是一個很有用的工具,讓你可以關注在你想處理的事情上面,並減少了重複性判斷,大大增加了程式碼的可讀性。

Ref.

swiftbysundell