標籤:swift ios
閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。 Swift 中的閉包與 C 和 Objective-C 中的代碼塊(blocks)以及其他一些程式設計語言中的 lambdas 函數比較相似。
閉包可以捕獲和儲存其所在上下文中任意常量和變數的引用。這就是所謂的閉合并包裹著這些常量和變數,俗稱閉包。Swift 會為您管理在捕獲過程中涉及到的所有記憶體操作。
注意:
如果您不熟悉捕獲(capturing)這個概念也不用擔心,您可以在值捕獲 章節對其進行詳細瞭解。
在函數章節中介紹的全域和嵌套函數實際上也是特殊的閉包,閉包採取如下三種形式之一:
全域函數是一個有名字但不會捕獲任何值的閉包
嵌套函數是一個有名字並可以捕獲其封閉函數域內值的閉包
閉包運算式是一個利用輕量級文法所寫的可以捕獲其上下文中變數或常量值的匿名閉包
Swift 的閉包運算式擁有簡潔的風格,並鼓勵在常見情境中進行文法最佳化,主要最佳化如下:
利用上下文推斷參數和返回值類型
隱式返回單運算式閉包,即單運算式閉包可以省略return關鍵字
參數名稱縮寫
尾隨(Trailing)閉包文法
閉包運算式(Closure Expressions)
嵌套函數是一個在較複雜函數中方便進行命名和定義自包含代碼模組的方式。當然,有時候撰寫小巧的沒有完整定義和命名的類函數結構也是很有用處的,尤其是在您處理一些函數並需要將另外一些函數作為該函數的參數時。
閉包運算式是一種利用簡潔文法構建內聯閉包的方式。閉包運算式提供了一些文法最佳化,使得撰寫閉包變得簡單明了。下面閉包運算式的例子通過使用幾次迭代展示了sort函數定義和文法最佳化的方式。每一次迭代都用更簡潔的方式描述了相同的功能。
sort 函數(The Sort Function)
Swift 標準庫提供了sort函數,會根據您提供的基於輸出類型排序的閉包函數將已知類型數組中的值進行排序。一旦排序完成,函數會返回一個與原數組大小相同的新數組,該數組中包含已經正確排序的同類型元素。
下面的閉包運算式樣本使用sort函數對一個String類型的數組進行字母逆序排序,以下是初始數組值:
let names = ["Chris","Alex", "Ewa", "Barry", "Daniella"]
sort函數需要傳入兩個參數:
已知類型的數組
閉包函數,該閉包函數需要傳入與數群組類型相同的兩個值,並返回一個布爾類型值來告訴sort函數當排序結束後傳入的第一個參數排在第二個參數前面還是後面。如果第一個參數值出現在第二個參數值前面,排序閉包函數需要返回true,反之返回false。
該例子對一個String類型的數組進行排序,因此排序閉包函數類型需為(String, String) -> Bool。
提供排序閉包函數的一種方式是撰寫一個符合其類型要求的普通函數,並將其作為sort函數的第二個參數傳入:
func backwards(s1: String, s2: String)-> Bool { return s1 > s2}var reversed = sort(names, backwards)// reversed 為 ["Ewa","Daniella", "Chris", "Barry", "Alex"]
如果第一個字串 (s1) 大於第二個字串 (s2),backwards函數返回true,表示在新的數組中s1應該出現在s2前。對於字串中的字元來說,“大於”表示 “按照字母順序較晚出現”。 這意味著字母"B"大於字母"A",字串"Tom"大於字串"Tim"。 其將進行字母逆序排序,"Barry"將會排在"Alex"之後。
然而,這是一個相當冗長的方式,本質上只是寫了一個單運算式函數 (a > b)。在下面的例子中,利用閉合運算式文法可以更好的構造一個內聯排序閉包。
閉包運算式文法(Closure Expression Syntax)
閉包運算式文法有如下一般形式:
{ (parameters) -> returnType in statements}
閉包運算式文法可以使用常量、變數和inout類型作為參數,不提供預設值。也可以在參數列表的最後使用可變參數。元組也可以作為參數和返回值。
下面的例子展示了之前backwards函數對應的閉包運算式版本的代碼:
reversed = sort(names, { (s1: String, s2:String) -> Bool in return s1 > s2 })
需要注意的是內聯閉包參數和返回值型別宣告與backwards函數型別宣告相同。在這兩種方式中,都寫成了(s1: String, s2: String) -> Bool。 然而在內聯閉包運算式中,函數和返回值類型都寫在大括弧內,而不是大括弧外。
閉包的函數體部分由關鍵字in引入。該關鍵字表示閉包的參數和返回值類型定義已經完成,閉包函數體即將開始。
因為這個閉包的函數體部分如此短以至於可以將其改寫成一行代碼:
reversed = sort(names, { (s1: String, s2:String) -> Bool in return s1 > s2 } )
這說明sort函數的整體調用保持不變,一對圓括弧仍然包裹住了函數中整個參數集合。而其中一個參數現在變成了內聯閉包(相比於backwards版本的代碼)。
根據上下文推斷類型(Inferring Type From Context)
因為排序閉包函數是作為sort函數的參數進行傳入的,Swift可以推斷其參數和返回值的類型。 sort期望第二個參數是類型為(String, String) -> Bool的函數,因此實際上String,String和Bool類型並不需要作為閉包運算式定義中的一部分。 因為所有的類型都可以被正確推斷,返回箭頭 (->) 和圍繞在參數周圍的括弧也可以被省略:
reversed = sort(names, { s1, s2 in returns1 > s2 } )
實際上任何情況下,通過內聯閉包運算式構造的閉包作為參數傳遞給函數時,都可以推斷出閉包的參數和返回值類型,這意味著您幾乎不需要利用完整格式構造任何內聯閉包。
單運算式閉包隱式返回(Implicit Return From Single-Expression Clossures)
單行運算式閉包可以通過隱藏return關鍵字來隱式返回單行運算式的結果,如上版本的例子可以改寫為:
reversed = sort(names, { s1, s2 in s1 >s2 } )
在這個例子中,sort函數的第二個參數函數類型明確了閉包必須返回一個Bool類型值。因為閉包函數體只包含了一個單一運算式 (s1 > s2),該運算式返回Bool類型值,因此這裡沒有歧義,return關鍵字可以省略。
參數名稱縮寫(Shorthand Argument Names)
Swift 自動為內嵌函式提供了參數名稱縮寫功能,您可以直接通過$0,$1,$2來順序調用閉包的參數。
如果您在閉包運算式中使用參數名稱縮寫,您可以在閉包參數列表中省略對其的定義,並且對應參數名稱縮寫的類型會通過函數類型進行推斷。 in關鍵字也同樣可以被省略,因為此時閉包運算式完全由閉包函數體構成:
reversed = sort(names, { $0 > $1 } )
在這個例子中,$0和$1表示閉包中第一個和第二個String類型的參數。
運算子函數(Operator Functions)
實際上還有一種更簡短的方式來撰寫上面例子中的閉包運算式。 Swift 的String類型定義了關於大於符號 (>) 的字串實現,其作為一個函數接受兩個String類型的參數並返回Bool類型的值。而這正好與sort函數的第二個參數需要的函數類型相符合。因此,您可以簡單地傳遞一個大於符號,Swift可以自動推斷出您想使用大於符號的字串函數實現:
reversed = sort(names, >)
更多關於運算子運算式的內容請查看運算子函數。
尾隨閉包(Trailing Closures)
如果您需要將一個很長的閉包運算式作為最後一個參數傳遞給函數,可以使用尾隨閉包來增強函數的可讀性。尾隨閉包是一個書寫在函數括弧之後的閉包運算式,函數支援將其作為最後一個參數調用。
func someFunctionThatTakesAClosure(closure:() -> ()) { // 函數體部分} // 以下是不使用尾隨閉包進行函數調用 someFunctionThatTakesAClosure({ // 閉包主體部分 }) // 以下是使用尾隨閉包進行函數調用 someFunctionThatTakesAClosure() { // 閉包主體部分}
注意:
如果函數只需要閉包運算式一個參數,當您使用尾隨閉包時,您甚至可以把()省略掉。
在上例中作為sort函數參數的字串排序閉包可以改寫為:
reversed = sort(names) { $0 > $1 }
當閉包非常長以至於不能在一行中進行書寫時,尾隨閉包變得非常有用。舉例來說,Swift 的Array類型有一個map方法,其擷取一個閉包運算式作為其唯一參數。數組中的每一個元素調用一次該閉包函數,並返回該元素所映射的值(也可以是不同類型的值)。具體的映射方式和返回值類型由閉包來指定。
當提供給數組閉包函數後,map方法將返回一個新的數組,數組中包含了與原數組一一對應的映射後的值。
下例介紹了如何在map方法中使用尾隨閉包將Int類型數組[16,58,510]轉換為包含對應String類型的數組["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]
如上代碼建立了一個數字位和他們名字映射的英文版本字典。同時定義了一個準備轉換為字串的整型數組。
您現在可以通過傳遞一個尾隨閉包給numbers的map方法來建立對應的字串版本數組。需要注意的時調用numbers.map不需要在map後麵包含任何括弧,因為其只需要傳遞閉包運算式這一個參數,並且該閉包運算式參數通過尾隨方式進行撰寫:
let strings = numbers.map { (var number) -> String in var output = "" while number > 0 { output = digitNames[number % 10]! + output number /= 10 } return output}// strings 常量被推斷為字串類型數組,即String[]// 其值為["OneSix", "FiveEight", "FiveOneZero"]
map在數組中為每一個元素調用了閉包運算式。 您不需要指定閉包的輸入參數number的類型,因為可以通過要映射的數群組類型進行推斷。
閉包number參數被聲明為一個變數參數(變數的具體描述請參看常量參數和變數參數),因此可以在閉包函數體內對其進行修改。閉包運算式制定了傳回型別為String,以表明儲存映射值的新數群組類型為String。
閉包運算式在每次被調用的時候建立了一個字串並返回。其使用求餘運算子 (number % 10) 計算最後一位元字並利用digitNames字典擷取所映射的字串。
注意:
字典digitNames下標後跟著一個歎號 (!),因為字典下標返回一個可選值 (optional value),表明即使該 key 不存在也不會尋找失敗。在上例中,它保證了number % 10可以總是作為一個digitNames字典的有效下標 key。因此歎號可以用於強制解析 (force-unwrap) 儲存在可選下標項中的String類型值。
從digitNames字典中擷取的字串被添加到輸出的前部,逆序建立了一個字串版本的數字。(在運算式number % 10中,如果number為16,則返回6,58返回8,510返回0)。
number變數之後除以10。 因為其是整數,在計算過程中未除盡部分被忽略。 因此 16變成了1,58變成了5,510變成了51。
整個過程重複進行,直到number /= 10為0,這時閉包會將字串輸出,而map函數則會將字串添加到所映射的數組中。
上例中尾隨閉包文法在函數後整潔封裝了具體的閉包功能,而不再需要將整個閉包包裹在map函數的括弧內。
捕獲值(Capturing Values)
閉包可以在其定義的上下文中捕獲常量或變數。即使定義這些常量和變數的原域已經不存在,閉包仍然可以在閉包函數體內引用和修改這些值。
Swift最簡單的閉包形式是嵌套函數,也就是定義在其他函數的函數體內的函數。 嵌套函數可以捕獲其外部函數所有的參數以及定義的常量和變數。
下例為一個叫做makeIncrementor的函數,其包含了一個叫做incrementor嵌套函數。嵌套函數incrementor從上下文中捕獲了兩個值,runningTotal和amount。之後makeIncrementor將incrementor作為閉包返回。每次調用incrementor時,其會以amount作為增量增加runningTotal的值。
func makeIncrementor(forIncrement amount:Int) -> () -> Int { var runningTotal = 0 func incrementor() -> Int { runningTotal += amount return runningTotal } return incrementor}
makeIncrementor傳回型別為() ->Int。 這意味著其返回的是一個函數,而不是一個簡單類型值。 該函數在每次調用時不接受參數只返回一個Int類型的值。關於函數返回其他函數的內容,請查看函數類型作為傳回型別。
makeIncrementor函數定義了一個整型變數runningTotal(初始為0) 用來儲存當前跑步總數。 該值通過incrementor返回。
makeIncrementor有一個Int類型的參數,其外部命名為forIncrement, 內部命名為amount,表示每次incrementor被調用時runningTotal將要增加的量。
incrementor函數用來執行實際的增加操作。 該函數簡單地使runningTotal增加amount,並將其返回。
如果我們單獨看這個函數,會發現看上去不同尋常:
func incrementor() -> Int { runningTotal += amount return runningTotal}
incrementor函數並沒有擷取任何參數,但是在函數體內訪問了runningTotal和amount變數。這是因為其通過捕獲在包含它的函數體內已經存在的runningTotal和amount變數而實現。
由於沒有修改amount變數,incrementor實際上捕獲並儲存了該變數的一個副本,而該副本隨著incrementor一同被儲存。
然而,因為每次調用該函數的時候都會修改runningTotal的值,incrementor捕獲了當前runningTotal變數的引用,而不是僅僅複製該變數的初始值。捕獲一個引用保證了當makeIncrementor結束時候並不會消失,也保證了當下一次執行incrementor函數時,runningTotal可以繼續增加。
注意:
Swift 會決定捕獲引用還是拷貝值。 您不需要標註amount或者runningTotal來聲明在嵌入的incrementor函數中的使用方式。Swift 同時也處理runingTotal變數的記憶體管理操作,如果不再被incrementor函數使用,則會被清除。
下面代碼為一個使用makeIncrementor的例子:
let incrementByTen =makeIncrementor(forIncrement: 10)
該例子定義了一個叫做incrementByTen的常量,該常量指向一個每次調用會加10的incrementor函數。調用這個函數多次可以得到以下結果:
incrementByTen()// 返回的值為10incrementByTen()// 返回的值為20incrementByTen()// 返回的值為30
如果您建立了另一個incrementor,其會有一個屬於自己的獨立的runningTotal變數的引用。下面的例子中,incrementBySevne捕獲了一個新的runningTotal變數,該變數和incrementByTen中捕獲的變數沒有任何聯絡:
let incrementBySeven =makeIncrementor(forIncrement: 7)incrementBySeven()// 返回的值為7incrementByTen()// 返回的值為40
注意:
如果您閉包分配給一個類執行個體的屬性,並且該閉包通過指向該執行個體或其成員來捕獲了該執行個體,您將建立一個在閉包和執行個體間的強引用環。 Swift 使用捕獲列表來打破這種強引用環。更多資訊,請參考閉包引起的迴圈強引用。
閉包是參考型別(Closures Are Reference Types)
上面的例子中,incrementBySeven和incrementByTen是常量,但是這些常量指向的閉包仍然可以增加其捕獲的變數值。這是因為函數和閉包都是參考型別。
無論您將函數/閉包賦值給一個常量還是變數,您實際上都是將常量/變數的值設定為對應函數/閉包的引用。上面的例子中,incrementByTen指向閉包的引用是一個常量,而並非閉包內容本身。
這也意味著如果您將閉包賦值給了兩個不同的常量/變數,兩個值都會指向同一個閉包:
let alsoIncrementByTen = incrementByTenalsoIncrementByTen()// 返回的值為50