Swift 閉包

Cocoma發表於2018-06-20

在OC中儲存一段程式碼塊可以使用Block,而對於Swift中也有相應的對照用於儲存程式碼塊這個就是今天所說的閉包,在其它語言中也叫匿名函式。

Swift 閉包

一、閉包

閉包是自包含的函式程式碼塊,可以在程式碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的程式碼塊(blocks)以及其他一些程式語言中的匿名函式比較相似。 閉包可以捕獲和儲存其所在上下文中任意常量和變數的引用。被稱為包裹常量和變數。 Swift 會為你管理在捕獲過程中涉及到的所有記憶體操作。

二、閉包表示式

巢狀函式是一個在較複雜函式中方便進行命名和定義自包含程式碼模組的方式。當然,有時候撰寫小巧的沒有完整定義和命名的類函式結構也是很有用處的,尤其是在處理一些函式並需要將另外一些函式作為該函式的引數時。

閉包表示式是一種利用簡潔語法構建內聯閉包的方式。閉包表示式提供了一些語法優化,使得撰寫閉包變得簡單明瞭。下面閉包表示式的例子通過使用幾次迭代展示了sort(_:)方法定義和語法優化的方式。每一次迭代都用更簡潔的方式描述了相同的功能。

三、sort 方法

Swift 標準庫提供了名為sort的方法,會根據提供的用於排序的閉包函式將已知型別陣列中的值進行排序。一旦排序完成,sort(_:)方法會返回一個與原陣列大小相同,包含同型別元素且元素已正確排序的新陣列。原陣列不會被sort(_:)方法修改。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
複製程式碼

sort(_:)方法接受一個閉包,該閉包函式需要傳入與陣列元素型別相同的兩個值,並返回一個布林型別值來表明當排序結束後傳入的第一個引數排在第二個引數前面還是後面。如果第一個引數值出現在第二個引數值前面,排序閉包函式需要返回true,反之返回false

該例子對一個String型別的陣列進行排序,因此排序閉包函式型別需為(String, String) -> Bool

提供排序閉包函式的一種方式是撰寫一個符合其型別要求的普通函式,並將其作為sort(_:)方法的引數傳入:

func backwards(s1: String, s2: String) -> Bool {
    return s1 > s2
}
var reversed = names.sort(backwards)
// reversed 為 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
複製程式碼

四、閉包表示式語法

閉包表示式語法有如下一般形式:

{ (parameters) -> returnType in
    statements
}
複製程式碼

閉包表示式語法可以使用常量、變數和inout型別作為引數,不能提供預設值。也可以在引數列表的最後使用可變引數。元組也可以作為引數和返回值。

下面的例子展示了之前backwards(_:_:)函式對應的閉包表示式版本的程式碼:

reversed = names.sort({ (s1: String, s2: String) -> Bool in
    return s1 > s2
})
複製程式碼

需要注意的是內聯閉包引數和返回值型別宣告與backwards(_:_:)函式型別宣告相同。在這兩種方式中,都寫成了(s1: String, s2: String) -> Bool。然而在內聯閉包表示式中,函式和返回值型別都寫在大括號內,而不是大括號外。

閉包的函式體部分由關鍵字in引入。該關鍵字表示閉包的引數和返回值型別定義已經完成,閉包函式體即將開始。

由於這個閉包的函式體部分如此短,以至於可以將其改寫成一行程式碼:

reversed = names.sort( { (s1: String, s2: String) -> Bool in return s1 > s2 } )
複製程式碼

該例中sort(_:)方法的整體呼叫保持不變,一對圓括號仍然包裹住了方法的整個引數。然而,引數現在變成了內聯閉包。

五、值捕獲

閉包可以在其被定義的上下文中捕獲常量或變數。即使定義這些常量和變數的原作用域已經不存在,閉包仍然可以在閉包函式體內引用和修改這些值。

Swift 中,可以捕獲值的閉包的最簡單形式是巢狀函式,也就是定義在其他函式的函式體內的函式。巢狀函式可以捕獲其外部函式所有的引數以及定義的常量和變數。

舉個例子,這有一個叫做 makeIncrementer的函式,其包含了一個叫做incrementer的巢狀函式。巢狀函式 incrementer()從上下文中捕獲了兩個值,runningTotalamount。捕獲這些值之後,makeIncrementerincrementer 作為閉包返回。每次呼叫 incrementer 時,其會以 amount 作為增量增加 runningTotal 的值

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}
複製程式碼

makeIncrementer 返回型別為() -> Int。這意味著其返回的是一個函式,而非一個簡單型別的值。該函式在每次呼叫時不接受引數,只返回一個 Int 型別的值。 makeIncrementer(forIncrement:) 函式定義了一個初始值為 0 的整型變數 runningTotal,用來儲存當前總計數值。該值為 incrementer 的返回值。

makeIncrementer(forIncrement:) 有一個 Int型別的引數,其外部引數名為 forIncrement,內部引數名為 amount,該參數列示每次 incrementer被呼叫時 runningTotal 將要增加的量。makeIncrementer 函式還定義了一個巢狀函式 incrementer,用來執行實際的增加操作。該函式簡單地使 runningTotal增加 amount,並將其返回。

如果我們單獨考慮巢狀函式incrementer(),會發現它有些不同尋常

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}
複製程式碼

incrementer() 函式並沒有任何引數,但是在函式體內訪問了 runningTotalamount 變數。這是因為它從外圍函式捕獲了 runningTotalamount 變數的引用。捕獲引用保證了 runningTotalamount變數在呼叫完 makeIncrementer 後不會消失,並且保證了在下一次執行incrementer 函式時,runningTotal 依舊存在。

注意 為了優化,如果一個值不會被閉包改變,或者在閉包建立後不會改變,Swift 可能會改為捕獲並儲存一份對值的拷貝。 Swift 也會負責被捕獲變數的所有記憶體管理工作,包括釋放不再需要的變數 下面是一個使用 makeIncrementer 的例子:

let incrementByTen = makeIncrementer(forIncrement: 10)
複製程式碼

該例子定義了一個叫做incrementByTen 的常量,該常量指向一個每次呼叫會將其runningTotal變數增加 10 的 incrementer 函式。呼叫這個函式多次可以得到以下結果:

incrementByTen()
// 返回的值為10
incrementByTen()
// 返回的值為20
incrementByTen()
// 返回的值為30
複製程式碼

如果你建立了另一個 incrementer,它會有屬於自己的引用,指向一個全新、獨立的 runningTotal 變數:

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回的值為7
複製程式碼

再次呼叫原來的incrementByTen 會繼續增加它自己的 runningTotal 變數,該變數和 incrementBySeven 中捕獲的變數沒有任何聯絡:

incrementByTen()
// 返回的值為40
複製程式碼

注意: 如果你將閉包賦值給一個類例項的屬性,並且該閉包通過訪問該例項或其成員而捕獲了該例項,你將在閉包和該例項間建立一個迴圈強引用。Swift 使用捕獲列表來打破這種迴圈強引用

六、閉包是引用型別

上面的例子中,incrementBySevenincrementByTen 都是常量,但是這些常量指向的閉包仍然可以增加其捕獲的變數的值。這是因為函式和閉包都是引用型別。

無論你將函式或閉包賦值給一個常量還是變數,你實際上都是將常量或變數的值設定為對應函式或閉包的引用。上面的例子中,指向閉包的引用 incrementByTen 是一個常量,而並非閉包內容本身。

這也意味著如果你將閉包賦值給了兩個不同的常量或變數,兩個值都會指向同一個閉包:

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值為50
複製程式碼

七、逃逸閉包

當一個閉包作為引數傳到一個函式中,但是這個閉包在函式返回之後才被執行,我們稱該閉包從函式中逃逸。當你定義接受閉包作為引數的函式時,你可以在引數名之前標註 @escaping,用來指明這個閉包是允許“逃逸”出這個函式的。

一種能使閉包“逃逸”出函式的方法是,將這個閉包儲存在一個函式外部定義的變數中。舉個例子,很多啟動非同步操作的函式接受一個閉包引數作為 completion handler。這類函式會在非同步操作開始之後立刻返回,但是閉包直到非同步操作結束後才會被呼叫。在這種情況下,閉包需要“逃逸”出函式,因為閉包需要在函式返回之後被呼叫。例如:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}
複製程式碼

someFunctionWithEscapingClosure(_:) 函式接受一個閉包作為引數,該閉包被新增到一個函式外定義的陣列中。如果你不將這個引數標記為 @escaping,就會得到一個編譯錯誤。

將一個閉包標記為 @escaping 意味著你必須在閉包中顯式地引用 self。比如說,在下面的程式碼中,傳遞到 someFunctionWithEscapingClosure(_:) 中的閉包是一個逃逸閉包,這意味著它需要顯式地引用self。相對的,傳遞到 someFunctionWithNonescapingClosure(_:) 中的閉包是一個非逃逸閉包,這意味著它可以隱式引用 self

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 列印出 "200"

completionHandlers.first?()
print(instance.x)
// 列印出 "100"
複製程式碼

八、自動閉包

自動閉包是一種自動建立的閉包,用於包裝傳遞給函式作為引數的表示式。這種閉包不接受任何引數,當它被呼叫的時候,會返回被包裝在其中的表示式的值。這種便利語法讓你能夠省略閉包的花括號,用一個普通的表示式來代替顯式的閉包。

我們經常會呼叫採用自動閉包的函式,但是很少去實現這樣的函式。舉個例子來說,assert(condition:message:file:line:)函式接受自動閉包作為它的 condition引數和 message 引數;它的 condition 引數僅會在 debug 模式下被求值,它的 message 引數僅當 condition 引數為 false 時被計算求值。

自動閉包讓你能夠延遲求值,因為直到你呼叫這個閉包,程式碼段才會被執行。延遲求值對於那些有副作用(Side Effect)和高計算成本的程式碼來說是很有益處的,因為它使得你能控制程式碼的執行時機。下面的程式碼展示了閉包如何延時求值。

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 列印出 "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 列印出 "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 列印出 "4"
複製程式碼

儘管在閉包的程式碼中,customersInLine 的第一個元素被移除了,不過在閉包被呼叫之前,這個元素是不會被移除的。如果這個閉包永遠不被呼叫,那麼在閉包裡面的表示式將永遠不會執行,那意味著列表中的元素永遠不會被移除。請注意,customerProvider 的型別不是 String,而是 () -> String,一個沒有引數且返回值為 String 的函式。

將閉包作為引數傳遞給函式時,你能獲得同樣的延時求值行為。

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 列印出 "Now serving Alex!"
複製程式碼

上面的 serve(customer:) 函式接受一個返回顧客名字的顯式的閉包。下面這個版本的 serve(customer:) 完成了相同的操作,不過它並沒有接受一個顯式的閉包,而是通過將引數標記為 @autoclosure來接收一個自動閉包。現在你可以將該函式當作接受 String型別引數(而非閉包)的函式來呼叫。customerProvider 引數將自動轉化為一個閉包,因為該引數被標記了 @autoclosure 特性。

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 列印 "Now serving Ewa!"
複製程式碼

注意 過度使用 autoclosures 會讓你的程式碼變得難以理解。上下文和函式名應該能夠清晰地表明求值是被延遲執行的。

如果你想讓一個自動閉包可以“逃逸”,則應該同時使用 @autoclosure@escaping 屬性

// customersInLine i= ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// 列印 "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// 列印 "Now serving Barry!"
// 列印 "Now serving Daniella!"
複製程式碼

相關文章