Swift 最佳實踐之 Closure
Swift 作為現代、高效、安全的編程語言,其背後有很多高級特性為之支撐。
『 Swift 最佳實踐 』系列對常用的語言特性逐個進行介紹,助力寫出更簡潔、更優雅的 Swift 代碼,快速實現從 OC 到 Swift 的轉變。
該系列內容主要包括:
- Optional
- Enum
- Closure
- Protocol
- Generic
- Property Wrapper
- Structured Concurrent
- Result builder
- Error Handle
- Advanced Collections (Asyncsequeue/OptionSet/Lazy)
- Expressible by Literal
- Pattern Matching
- Metatypes(.self/.Type/.Protocol)
ps. 本系列不是入門級語法教程,需要有一定的 Swift 基礎
本文是系列文章的第三篇,介紹 Closure,內容主要包括如何利用 Inferring Type 簡化閉包的使用、escaping-closure 與 nonescaping-closure 的區別、Capture List 注意事項、Trailing Closures 以及 Auto Closures 等。
Overview
Swift Closure 與 Objective-C Block 有很多相似之處,都屬於匿名函數 / lambdas-expressions 的範疇。
相比之下,Swift Closure 更安全、更簡潔。
首先,簡要回顧一下閉包的基本語法,Closure 完整定義如下,幾個關鍵組成:
- 參數列表
- 返回值類型
- 關鍵字
in
- closure body statements
swift
{ (parameters) -> type in
statements
}
聲明 Closure 變量:
swift
let closure: (parameters) -> type
看個簡單的例子,如下,為數組排序方法 sorted
傳入了用於排序操作的 closure,其類型為:(String, String) -> Bool
,即有 2 個 String
類型的參數,返回值為 Bool
:
swift
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
let reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
由於 closure body 只有一行代碼,故可以直接將其放在 in
後面:
swift
names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
Inferring Type
得益於 Swift 強大的類型推演能力,上述排序閉包可以簡化為:
swift
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
即不用顯式寫明參數、返回值的類型,編譯器根據上下文完全可以推演出來
從 Swift 5.1 起,對於只有一個表達式 (Single-expression) 的方法 / 閉包,會隱式返回該表達式的值 swift-evolution/0255-omit-return · GitHub
簡單講,就是對於 Single-expression 的方法 / 閉包可以省略 return
關鍵字:
swift
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
Swift 為 Closure 參數提供了一種簡寫方式,即分別用 $0
、$1
來表示參數列表中的參數,此時可以忽略閉包參數列表,如下:
```swift reversedNames = names.sorted(by: { $0 > $1 } )
// 如後文所述還有更簡潔的版本 // reversedNames = names.sorted(by: >) ```
對於只有一個參數的閉包可以使用參數的簡寫形式
$0
對於有 2 個及以上參數的情況慎用簡寫形式,可能會影響代碼可讀性
Escaping Closures
escaping-closure、nonescaping-closure 是 Swift Closure 相較 OC Block 出現的一個新概念。
escaping、nonescaping 描述的是 Closure 作為方法參數時的分類:
-
當作為參數的 Closure,其生命週期不會逃逸出所在方法時,稱為 nonescaping-closure,(意味着該閉包在方法調用鏈上會被執行),如:
swift func foo(_ closure: () -> Void) { // closure 沒有逃逸出 foo closure() }
如下,雖在方法
bar
中沒有直接執行closure
,但在其調用鏈上的foo
會執行closure
,closure
並沒有逃逸出bar
:swift func bar(_ closure: () -> Void) { foo(closure) }
-
當 Closure 的生命週期逃逸出所在方法時,稱為 escaping-closure。
如下,
foo
將參數escapingClosure
存儲在屬性closure
中,使其逃逸出foo
,即foo
返回後escapingClosure
還存在:```swift public class EscapingClosureDemo { var closure: (() -> Void)?
func foo(_ escapingClosure: @escaping () -> Void) { closure = escapingClosure } } ```
此時,參數
escapingClosure
的定義須加上關鍵字@escaping
,顯式表明定義的是 escaping-closure,否則編譯報錯:OC 中相當於所有 block 默認都是 escaping
在 Swift 3 以前, 閉包類型的參數默認是 escaping,並提供了
@noescape
關鍵字用於聲明 nonescaping Closure但從 Swift 3 開始,閉包參數默認是 nonescaping,對於 escaping closure 須顯式聲明
@escaping
,並廢棄了@noescape
Why❓
為什麼要區分 escaping、nonescaping,並在 Swift 3 中將默認值從 escaping 改成 nonescaping?
主要原因有兩個:
- 編譯器優化 👍,對於 escaping closure 需要更復雜的內存管理,而 nonescaping closure 編譯器可以做優化
- 顯式提醒開發人員 ⚡️:「你正在危險的邊緣試探——正在定義 / 調用的是 escaping closure!」
escaping-closure 為何就危險了❓
原因在於 escaping-closure 可能會產生循環引用 (Strong Reference Cycles),而 nonescaping closure 一定是不會有循環引用的。
因此,在 escaping-closure 中不允許隱式捕獲 self
,以免在「不經意間 😴」引起循環引用:
Capturing Values
我們從一個簡單的問題開始:Can You Answer This Simple Swift Question Correctly?
在 1334 位回答者中只有 44% 回答正確 🙈
正確答案:
- 1 -- Objc
- 2 -- Swift
1 和 2 的區別在於:1 用了捕獲列表 (Capture List),而 2 沒有。
Capture List:
-
用
[]
聲明的表達式列表,表達式間用,
分隔,放在參數列表前 (如有):```swift func bar() { var age = 10 var name = "Jim"
let closure = { [age, name] in // 等價於 [age = age, name = name] // 此處的 age、name 與閉包外的已沒任何關係,僅名字相同而以 print("(name) is (age) years old!") }
age = 11 name = "Tom" closure() // Jim is 10 years old! } ```
-
capture list 在閉包定義時完成初始化賦值 (
[age = age, name = name]
)所以上面輸出的是
Jim is 10 years old!
,而不是Tom is 11 years old!
如果不用 capture list,而是直接引用,則直到 closure 執行時才去取值:
but,如果捕獲的是引用類型 (reference types),那情況就不一樣了:
String 在 Swift 中是值類型,而非引用類型
```swift class Foo { var age: Int var name: String
init(age: Int, name: String) { self.age = age self.name = name } }
func bar() { var foo = Foo(age: 10, name: "Jim") let closure = { [foo] in // 捕獲引用類型 (foo) print("(foo.name) is (foo.age) years old!") }
foo.age = 11 foo.name = "Tom"
closure() // Tom is 11 years old! } ```
雖然,捕獲引用類型時,其輸出有所不同,但「capture list 在閉包定義時完成初始化賦值」的特性並沒有變,只不過此時賦值的是「指針」
如下,若給
foo
賦一個新值,則與 closure 內捕獲的就沒任何關係了:```swift func bar() { var foo = Foo(age: 10, name: "Jim") let closure = { [foo] in print("(foo.name) is (foo.age) years old!") }
foo = Foo(age: 11, name: "Tom") // 此時,foo 指向新實例
closure() // Jim is 10 years old! } ```
-
通過 capture list 還可以指定捕獲類型:
strong
(默認)、weak
以及unowned
,用於避免循環引用:```swift { print(self.name) } // implicit strong capture { [self] in print(self.name) } // explicit strong capture { [weak self] in print(self?.name) } // weak capture { [unowned self] in print(self.name) } // unowned capture
// 還可以在 capture list 用表達式賦值 { [name = self.name] in print(name)} // strong capture name ```
總之,capture list 在閉包定義時完成初始化賦值 (而非執行時):
- 對於值類型 (value types),capture value 初始化實質上就是 copy,從此以後閉包內外的值就分道揚鑣,沒任何關係了 (只是名字相同而已)
- 對於引用類型 (reference types),capture value 初始化 copy 的實質上是個指針,閉包內外引用的還是同一個實例
Trailing Closures
如果方法的最後一個參數是 closure,那麼在調用時可以簡化:
```swift func foo() { // 正常調用 bar(doSomething: { print("normal call!") })
// trailing closure call // 省略()、label bar { print("trailing closure call!") } }
func bar(doSomething: () -> Void) { doSomething() } ```
對於有多個 closure 參數的情況,建議只對最後一個參數用 trailing closure call,以免影響可讀性:
```swift func foo() { // ❎ 不建議 bar { print("1") } doSomething1: { print("2") }
// ✅ bar(doSomething: { print("1") }) { print("2") } }
func bar(doSomething: () -> Void, doSomething1: () -> Void) { doSomething() doSomething1() } ```
Auto closures
如下,給 closure 加上 @autoclosure
後,在調用時可以直接用表達式,傳入的表達式會自動封裝成 closure,而無需顯式的寫成閉包的形式:
```swift func foo() { bar(doSomething: print("a")) // ❌ bar(doSomething: { print("a") }) baz(value: 1) // ❌ baz(value: { 1 }) }
func bar(doSomething: @autoclosure () -> Void) { print("b") doSomething() }
func baz(value: @autoclosure () -> Int) { print(value()) }
// 輸出: // b // a // 1 ```
- 對 closure 加
@autoclosure
的前提是其沒有參數 - 表達式是 lazy,只有到閉包執行時才執行表達式,而非方法調用時
正是由於 @autoclosure
的 lazy 特性,其常被用於那些期望懶加載的場景,如:Swift 標準庫提供的 assert
函數:
swift
public func assert(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String = String(),
file: StaticString = #file, line: UInt = #line
) {
// Only assert in debug mode.
if _isDebugAssertConfiguration() {
if !_fastPath(condition()) {
_assertionFailure("Assertion failed", message(), file: file, line: line, flags: _fatalErrorFlags())
}
}
}
由於 condition
、message
是 auto closure,故可以簡化 assert
調用:
assert(someCondition(), "Failed!")
如果它們是常規閉包,則調用時有點麻煩:
assert({ someCondition() }, { "Failed!" })
類似的,如 Alamofire 中對 Error 的擴展:
在項目中經常會對 Dictionary
的取值做些保護並提供默認值,如:
swift
extension Dictionary {
func value<T>(forKey key: Key, defaultValue: @autoclosure () -> T) -> T {
self[key].flatMap { $0 as? T } ?? defaultValue()
}
}
First-class functions first, closures second
這一小節有點挖牆腳的意思:「優先考慮一等函數,其次才是閉包」,前提是閉包參數與函數參數匹配,如:
```swift let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
// closure let reversedNames = names.sorted(by: { $0 > $1 } )
// first-class function let reversedNames = names.sorted(by: >) ```
```swift let domain: String? = "docs.swift.org"
// if let var url: URL? if let domain { url = URL(string: domain) }
// closure let url = domain.flatMap { URL(string: $0) }
// first-class function let url = domain.flatMap(URL.init) // 等價於 domain.flatMap(URL.init(string:)) ```
```swift let subviews: [UIView] = [...]
// closure subviews.forEach { addSubview($0) }
// first-class functions subviews.forEach(addSubview) ```
```swift let ages = [1, 2, 3]
// closure ages.forEach { baz(value: $0) }
// first-class functions ages.forEach(baz(value:))
func baz(value: Int) { print(value) } ```
小結
本文對 Closure 的主要特性以及最佳實踐進行了簡要分析介紹。
如,利用 Inferring Type 可以簡化閉包的使用,Capture List 對於值類型和引用類型的區別,Trailing Closures 以及 Auto closures 的使用等。
最後,提到某些場景下利用函數一等公民身份相比閉包可以簡化代碼。
參考資料
Swift clip: First class functions
Swift’s closure capturing mechanics
Using @autoclosure when designing Swift APIs
swift-evolution/0255-omit-return · GitHub
- Swift 最佳實踐之 Closure
- Swift 最佳實踐之 Optional
- Swift 最佳實踐之 Enum
- 深入淺出 Flutter Framework 之自定義渲染型 Widget
- 深入淺出 Flutter Framework 之 RenderObject
- Swift 新併發框架之 Task
- Swift 新併發框架之 actor
- Swift 新併發框架之 Sendable
- Swift 新併發框架之 async/await
- Swift Protocol 背後的故事(上)
- 『碼』出高質量
- 深入淺出 Flutter Framework 之 PipelineOwner
- 深入淺出 Flutter Framework 之 Layer
- 深入淺出 Flutter Framework 之 PaintingContext
- 深入淺出 Flutter Framework 之 Element