Swift 閉包 (Closures)

閉包 (Closures) 是自包含的程式碼塊,可以在程式碼中被傳遞和使用。Swift 的閉包類似於 C 和 Objective-C 的 Block,以及其他語言中的 Lambda。

閉包可以捕獲 (Capture) 和儲存其所在上下文中的常數和變數的引用,這就是所謂的「閉合包裹變數」,因此得名「閉包」。

閉包表達式語法

一般的函式其實就是一種特殊的閉包。而我們常說的「閉包表達式」通常指這種輕量級的語法:

{ (parameters) -> returnType in
    statements
}

例子:Swift 標準庫的 sorted(by:) 方法接受一個閉包來決定排序規則。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// 完整的閉包語法
var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

語法優化

Swift 的閉包語法在設計上非常簡潔,可以進行多次簡化:

1. 推斷型別

因為 sorted(by:) 預期接收 (String, String) -> Bool 的閉包,所以我們可以省略型別:

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

2. 單表達式隱式回傳

單行閉包可以省略 return

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })

3. 參數名稱縮寫

Swift 自動為閉包提供參數縮寫 $0, $1, $2 等:

reversedNames = names.sorted(by: { $0 > $1 })

4. 尾隨閉包 (Trailing Closures)

這是 Swift 最常見的寫法。如果閉包是函式的最後一個參數,你可以將閉包寫在括號外面:

reversedNames = names.sorted() { $0 > $1 }

如果函式只有這一個閉包參數,甚至連括號都可以省略:

reversedNames = names.sorted { $0 > $1 }

5. 多個尾隨閉包 (Multiple Trailing Closures)

從 Swift 5.3 開始,如果函式接受多個閉包參數,我們可以將它們都寫在括號外面。這在 SwiftUI 中非常常見。

第一個尾隨閉包省略參數標籤 (Label),隨後的閉包則必須寫上參數標籤。

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

// 呼叫方式:
loadPicture(from: someServer) { picture in
    // 第一個閉包 (completion)
    someView.currentPicture = picture
} onFailure: {
    // 第二個閉包 (onFailure)
    print("無法下載圖片")
}

這種語法讓程式碼讀起來更像是句子,結構更清晰。

捕獲值 (Capturing Values)

閉包可以在其定義的上下文中捕獲 (Capture) 常數或變數。即使定義這些常數和變數的原作用域已經不存在了 (例如函式已經執行完畢返回了),閉包仍然可以在其內部引用和修改這些值。

Swift 中最簡單的例子是嵌套函式 (Nested Function)。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    
    // incrementer 是一個閉包 (嵌套函式)
    func incrementer() -> Int {
        // 它捕獲了外部的 runningTotal 和 amount
        runningTotal += amount
        return runningTotal
    }
    
    return incrementer
}

在這個例子中,incrementer 函式並沒有任何參數,它的 runningTotalamount 都是從外部捕獲來的。

閉包是參考型別 (Reference Types)

當你將閉包賦值給變數或常數時,你實際上是在賦值一個引用 (Reference)。這意味著如果你將同一個閉包賦值給兩個不同的變數,它們會指向同一個閉包實例,共享同一個捕獲的狀態。

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen()) // 回傳 10
print(incrementByTen()) // 回傳 20
print(incrementByTen()) // 回傳 30

// 建立一個新的 incrementer
let incrementBySeven = makeIncrementer(forIncrement: 7)
print(incrementBySeven()) // 回傳 7 (獨立的狀態)

// 再次呼叫原本的 incrementer
print(incrementByTen()) // 回傳 40 (狀態繼續累積)

逃逸閉包 (Escaping Closures)

當一個閉包作為參數傳入函式,但在函式返回之後才被執行 (例如非同步操作的回呼 Callback),我們必須將該閉包標記為 @escaping

var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    // 閉包被儲存在陣列中,將在函式結束後才被呼叫
    completionHandlers.append(completionHandler)
}

自動閉包 (Autoclosures)

@autoclosure 是一種語法糖,它讓你傳遞一個表達式作為參數,Swift 會自動把它包裝成一個閉包。這常用於延遲求值 (Lazy Evaluation)。

// 參數是 () -> Bool,但使用 @autoclosure
func logIfTrue(_ predicate: @autoclosure () -> Bool) {
    if predicate() {
        print("True!")
    }
}

// 呼叫時看起來像傳入普通布林值,但其實 2 > 1 直到函式內部調用 predicate() 時才執行
logIfTrue(2 > 1)

閉包是 Swift 功能強大且優雅的關鍵,在處理集合操作 (map, filter, reduce) 和非同步程式設計時無處不在。