The Swift Programming Language-官方教程精譯Swift(8)閉包 -- Closures

來源:互聯網
上載者:User

標籤:style   blog   http   color   使用   os   io   strong   

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

 

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.