標籤:
閉包
閉包是指能夠在代碼中使用和傳遞的自包含(self-contained)的塊。
Swift中的閉包和C以及OC中的塊很相似。它們可以捕獲並且儲存定義它們的上下文中的任何常量和變數的引用。Swift會處理捕獲的記憶體管理,不需要開發人員操作。
前面介紹的全域函數和嵌套函數,其實就是閉包的特例,閉包有以下三種形式:
1、全域函數是具有名字並且不捕獲任何值的閉包
2、嵌套函數是具有名字並且能夠從包含它的函數內部捕獲值的閉包
3、閉包運算式是用輕量文法編寫的沒有名字切能從包含它的上下文中捕獲值的閉包
Swift的閉包運算式風格十分簡潔明確,並且做了很多最佳化,以鼓勵在一般情況下使用簡潔自由的文法,最佳化如下:
從上下文中偵測參數和傳回值類型
單語句閉包隱式返回
參數名稱簡寫
後置閉包文法
閉包運算式
嵌套函數就是一個作為更大函數的一部分的字包含的具名代碼塊的很好表現形式。但是,有時可能需要使用更簡便的形式,比如沒有名稱和全部聲明的構造。這在當你的操作那些接收別的函數當做參數的函數時尤為明顯。
閉包運算式提供了一種簡潔明確的文法來書寫內聯的閉包。閉包運算式提供了很多文法最佳化來使得簡寫形式的閉包不會變得意向模糊。
下邊樣本通過用越來越簡潔的方式不斷改進一個簡單的sorted方法,來展現了閉包運算式的這種最佳化。
sorted方法是Swift的標準庫提供的一個方法,它根據你提供的一個閉包來將已知類型的數組的元素進行排列。它接受兩個參數,第一個是已知類型的數組,另一個是閉包,這個閉包接受兩個同類型的參數,並且返回一個布爾值以確定第一個參數是否應該排在第二個參數前面。
假設一個數組是String類型的,那麼這個sorted函數的第二個參數閉包就應該是(String,String) -> Bool函數類型。
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
第一種提供排序閉包的方法是編寫一個類型正確的函數,然後將函數作為第二個參數傳遞給sorted函數:
func backwards(s1: String, s2: String) -> Bool { return s1 > s2}var reversed = sorted(names, backwards)// reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
當然,這種方式的寫法比較繁瑣,這個執行個體中,推薦使用閉包運算式文法編寫更簡潔的內聯排序閉包。
閉包運算式的一般形式是:
{ (parameters) -> return type in statements}
閉包運算式文法可以使用變數參數、常量參數、inout參數。但是不能給參數提供預設值。可以在參數列表的最後給可變參數指定名稱以使用可變參數。元組可以作為參數類型及傳回值類型。
如下是上個例子中排序閉包的閉包運算式版本:
reversed = sorted(names, { (s1: String, s2: String) -> Bool in return s1 > s2})
注意,這裡參數類型和傳回值類型的寫法與上個例子中的函數是有區別的,因為這裡是寫在大括弧裡邊而非外邊的。
這裡的in關鍵字用以將兩部分分開,標誌這參數及傳回值類型已經定義完畢,接下來是閉包體部分。這裡in後邊的換行並不是必須的,比如例子中閉包體很短,可以唯寫一行:
reversed = sorted(names, { (s1: String, s2: String) -> Bool in return s1 > s2 } )
從上下文中推斷類型
因為排序閉包是作為一個參數傳遞給函數的,Swift能夠推斷出它的參數類型以及它從sorted函數的第二個參數的類型返回的值的類型。這個參數期望一個(String, String) -> Bool類型的函數,這意味著(String, String) 和 Bool 並不需要在閉包運算式的定義中寫出來。因此更簡潔的版本如下:
reversed = sorted(names, { s1, s2 in return s1 > s2 } )
當以閉包運算式的形式將一個閉包傳遞給一個函數的時候,其參數類型和傳回值類型總是可以被推斷出的。因此,當閉包被作為函數的參數時,都不需要寫出它的完全形式。當然,如果在簡寫形式會造成語意不明的時候,推薦顯示地寫出完全形式。
單語句閉包的隱式返回
單語句閉包可以省略return關鍵字而隱式地返回結果,因此,可以進一步簡化為:
reversed = sorted(names, { s1, s2 in s1 > s2 } )
參數名稱簡寫
Swift自動給內聯閉包提供參數名稱的簡寫形式,通過$0,$1,$2...來指代第1,2,3...個參數。因此,排序閉包就可以簡寫為:
reversed = sorted(names, { $0 > $1 } )
這裡,$0和$1指代了第一個和第二個String。
運算子函數
上面樣本中的閉包運算式還可以更為簡潔,Swift的String類型定義了“大於運算子(>)”的string版本的實現。這個實現是一個接受兩個String型別參數並返回一個布爾類型的函數。這正好滿足了sorted函數的第二個參數所需要的函數類型。因此,你可以只傳遞“大於運算子”進去,Swift會推斷出你是想使用它的String版本的實現:
reversed = sorted(names, >)
後置閉包
如果你想將一個閉包運算式作為最後一個參數傳遞給某個函數,而且這個閉包表單時又很長的時候,就比較適合以閉包後置方式編寫代碼。後置閉包是指寫在它支援的函數調用的圓括弧後邊的閉包運算式。比如:
func someFunctionThatTakesAClosure(closure: () -> ()) { // function body goes here} // here‘s how you call this function without using a trailing closure: someFunctionThatTakesAClosure({ // closure‘s body goes here}) // here‘s how you call this function with a trailing closure instead: someFunctionThatTakesAClosure() { // trailing closure‘s body goes here}
因此,排序閉包可以進一步簡寫為:
reversed = sorted(names) { $0 > $1 }
注意:如果閉包運算式是作為函數的唯一參數並且用後置的文法編寫,那麼可以省略函數名稱後面的圓括弧。
當閉包運算式特別長的時候,後置閉包就顯得特別有用。比如Swift的Array有一個map(_:)方法,它接受一個閉包運算式作為它唯一的參數,這個閉包在數組的每個元素上都被運行一次,返回該元素對應的新元素,map方法的實現以及傳回值類型都留給了閉包去決定。下邊用閉包後置的寫法使用map(_:)方法,在下邊的例子中,數組[16, 58, 510]被用來建立新數組["OneSix", "FiveEight", "FiveOneZero"]。
let digitNames = [ 0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four", 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"]let numbers = [16, 58, 510]let strings = numbers.map { (var number) -> String in var output = "" while number > 0 { output = digitNames[number % 10]! + output number /= 10 } return output}// strings is inferred to be of type [String]// its value is ["OneSix", "FiveEight", "FiveOneZero"]
注意,這裡的map後邊省略了圓括弧,因為閉包運算式是作為唯一參數被傳入的;在閉包的參數列表中,使用了變數參數,這樣就不用在閉包體內部在建立局部變數number然後把參數值賦給它。
這裡還有一點需要注意的是 digitNames[number % 10]! 這裡的驚嘆號,前面介紹過,字典的下標文法返回的是一個可選類型(Optional Type),這雷根據上下文是可以確定這個值一定存在的,因此使用驚嘆號強制展開這個可選類型。
捕獲值
閉包可以從它被定義的上下文中捕獲變數或者常量。然後閉包就可以在自己的體內部引用或者改變這些常量或者變數,甚至在定義這些變數和常量的原始範圍已經不存在的情況下也是可以的。最簡單的例子就是嵌套函數,函數內部定義的函數可以捕獲外部函數內部的變數和常量,如果這個函數被返回了,它讓然可以引用和操作這些常量和變數,儘管此時外部函數已經被返回不存在了。比如:
func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer}
這裡incrementer被外層函數返回之後,讓然可以擷取和操作runningTotal和amount,這裡其實它捕獲的是runningTotal的引用而不是值拷貝,因此在每次被調用的時候,都是更改的同一個變數,而amount並沒有被改變,因此它是以值拷貝的形式被捕獲的。
Swift會自動決定那些變數需要捕獲引用,哪些需要捕獲值拷貝,並且所有的記憶體管理都是Swift自動管理的,不需要開發人員手動管理。
注意:如果你把一個閉包賦值給了一個類執行個體的屬性,而這個閉包又通過引用那個執行個體或者它的成員而引用它本身,那麼就在閉包和這個類執行個體之間形成了強引用迴圈。Swift通過引用列表來打破這種強引用迴圈,後面會有介紹。
閉包是參考型別
當把閉包賦值給某個常量的時候,仍然可以通過閉包改變它捕獲的那些變數,這是因為函數和閉包是參考型別的,在賦值的時候,只是把它們的引用地址賦值給了變數,而不是其內部的代碼塊。就比如如果把一個閉包賦值給了兩個常量,那麼這兩個常量其實指向了同一個閉包。
Swift學習筆記九