第九章 閉包
閉包是一段自包含的功能,可以被傳遞或者在代碼中使用。Swift 中的閉包與 C 或 Objective-C 中的 Blocks 或者一些其他語言中的 lambdas 很相似。
閉包可以捕獲到其所在上下文中的任何常量或變量的引用。就是將常量和變量包裹住,因此稱為 “閉包”。Swift 會(huì)為你處理捕獲過程中所涉及到的內(nèi)存管理任務(wù)。
注意:
如果你不熟悉 “捕獲” 的概念也不必?fù)?dān)心,在后面的 “捕獲值”(Capture Values)這個(gè)章節(jié)中會(huì)進(jìn)行詳細(xì)的講解。
在“函數(shù)”這一章中介紹的全局函數(shù)和嵌套函數(shù)是實(shí)際上是閉包的一種特殊情況。閉包有如下三種形式:
? ? ?1. 全局函數(shù)是一個(gè)有名稱且不捕獲任何值的閉包;
? ? ?2. 嵌套函數(shù)是一個(gè)有名稱且在其包裹函數(shù)中可以捕獲一些值的閉包;
? ? ?3. 閉包表達(dá)式是用輕量級(jí)語法所以寫的可以在其上下文中捕獲值的閉包;
Swift 閉包表達(dá)式的語法風(fēng)格簡(jiǎn)潔清晰,并且對(duì)于常見場(chǎng)景都有優(yōu)化使得語法更簡(jiǎn)潔。這些優(yōu)化包括:
? ? ?1. 從上下文推斷參數(shù)類型和返回值類型;
?? ? 2. 單表達(dá)式的閉包隱式返回;
? ? ?3. 參數(shù)名簡(jiǎn)寫;
? ? ?4. Trailing 閉包語法
9.1 閉包表達(dá)式
前面介紹過的嵌套函數(shù),是一種可以方便地在復(fù)雜函數(shù)中命名一段自包含代碼塊的方式。有時(shí)在編寫無完整定義和命名的類似函數(shù)的結(jié)構(gòu)時(shí)也很有用,尤其在是處理以一個(gè)或多個(gè)函數(shù)作為參數(shù)的函數(shù)時(shí)。
閉包表達(dá)式可以用簡(jiǎn)潔的語法編寫內(nèi)聯(lián)閉包。閉包表達(dá)式提供了一些語法優(yōu)化,可以以最簡(jiǎn)單的方式來編寫閉包,并且不丟失準(zhǔn)確性。下面的閉包表達(dá)式例子通過多次迭代精簡(jiǎn) sorted 函數(shù)來展示這些語法優(yōu)化,每一個(gè)例子都展示了用更高效的方式來編寫相同的功能。
Sorted 函數(shù)
Swift 標(biāo)準(zhǔn)庫中提供了 sorted 函數(shù),會(huì)根據(jù)你提供的排序閉包來對(duì)已知類型的數(shù)組進(jìn)行排序。當(dāng)排序完成時(shí),sorted 函數(shù)返回一個(gè)與原數(shù)組大小類型相同、元素已經(jīng)排列有序的新數(shù)組。原數(shù)組不會(huì)被 sorted 函數(shù)修改。
下面的例子中,閉包表達(dá)式使用 sorted 函數(shù)將一個(gè) String 數(shù)組按照字典序逆序方式排序。如下是用于排序的原始數(shù)組:
<此處添加代碼2.7.1- 1>
sorted 函數(shù)接收兩個(gè)參數(shù):
? ? ?1. 一個(gè)已知類型的數(shù)組
?? ? 2. 一個(gè)閉包,接收兩個(gè)與數(shù)組元素類型相同的參數(shù),并且返回一個(gè) Bool 值,代表的是第一個(gè)元素是否應(yīng)該排列在第二個(gè)元素前面。
這個(gè)例子要排序的是 String 類型的數(shù)組,所以閉包的函數(shù)類型是 (String, String) -> Bool。
提供閉包的一種方式是編寫一個(gè)相符類型的函數(shù),并將其作為 sort 函數(shù)的第二個(gè)參數(shù)傳入:
<此處添加代碼2.7.1- 2>
如果第一個(gè)字符串(s1)大于第二個(gè)字符串(s2),bcakwards 函數(shù)返回 true,表示在排好序的數(shù)組中 s1 出現(xiàn)在 s2 之前。對(duì)于字符串中的字符,“大于” 的含義是 “按字典序出現(xiàn)在后面”。
這意味著字母 “B” 大于字母 “A”,字符串 “Tom” 大于字符串 “Tim”。這個(gè)函數(shù)進(jìn)行字典序逆序排列,比如 “Barry” 或出現(xiàn)在 “Alex” 之前。
但這個(gè)書寫方式很繁瑣,實(shí)際上只相當(dāng)于寫了一個(gè)單表達(dá)式函數(shù) (a > b)。在下面的例子中,使用閉包表達(dá)式語法可以更好的構(gòu)造內(nèi)聯(lián)閉包。
閉包表達(dá)式語法
閉包表達(dá)式的形式通常如下:
<此處添加代碼2.7.1- 3>
閉包表達(dá)式語法可以使用常量、變量和 inout 形參。但不能供默認(rèn)值。可以在參數(shù)列表的末尾使用可變形參。元組可以作為形參或者返回類型。
下面的例子展示了之前的 backwards 函數(shù)的閉包表達(dá)式版本:
<此處添加代碼2.7.1- 4>
需要注意,內(nèi)聯(lián)閉包參數(shù)和返回類型聲明與 backwards 函數(shù)類型聲明是相同的。在這兩個(gè)例子中都是?(s1: String, s2: String) -> Bool 類型。但對(duì)于內(nèi)聯(lián)閉包表達(dá)式中,函數(shù)和返回值類型都寫在大括號(hào)內(nèi),而不是大括號(hào)外。
閉包的函數(shù)體部分由關(guān)鍵字 in 引入。該關(guān)鍵字表示閉包的參數(shù)和返回值類型定義已經(jīng)完成,閉包函數(shù)體即將開始。
因?yàn)檫@個(gè)閉包的函數(shù)主體非常短,因此可以改寫成一行:
<此處添加代碼2.7.1- 5>
這表明?sorted 函數(shù)的調(diào)用可以保持不變,圓括號(hào)內(nèi)仍然包含了所有參數(shù)。只是其中一個(gè)參數(shù)現(xiàn)在變成了內(nèi)聯(lián)閉包。?
根據(jù)上下文推斷類型
因?yàn)榕判蜷]包是作為一個(gè)參數(shù)傳入函數(shù)的,Swift 可以推斷其參數(shù)類型和返回值類型。第二個(gè)參數(shù)是 (String, String) -> Bool 類型的函數(shù),也就是說 String,String 和 Bool 類型不需要寫到閉包表達(dá)式定義中。 因?yàn)樗械念愋投伎梢员徽_推斷,,返回箭頭 (->) 和括號(hào)也可以省略:?
<此處添加代碼2.7.1- 6>
實(shí)際上在任何情況下,用內(nèi)聯(lián)閉包表達(dá)式構(gòu)造的閉包作為參數(shù)傳遞給函數(shù)時(shí),都可以推斷出其參數(shù)和返回值類型,因此,你幾乎不需要寫出完整格式來構(gòu)造內(nèi)聯(lián)閉包。
然而,你也可以使用明確的類型,這也是鼓勵(lì)做法,因?yàn)檫@樣可以避免閱讀代碼時(shí)可能存在的歧義。這個(gè)排序函數(shù)例子中,閉包的目的是很明確的,即排序。并且讀者可以放心的假設(shè)閉包會(huì)處理字符串值,因?yàn)樗怯糜趨f(xié)助對(duì)字符串?dāng)?shù)組排序的。
隱式返回的單表達(dá)式閉包
單表達(dá)式可以省略 return 關(guān)鍵字來隱式返回結(jié)果,就像上面的例子:
<此處添加代碼2.7.1- 7>
這里的?sorted 函數(shù)的第二個(gè)函數(shù)類型參數(shù)明確了閉包會(huì)返回一個(gè)?Bool 類型值。 因?yàn)殚]包函數(shù)主體只包含了一個(gè)表達(dá)式 (s1 > s2),該表達(dá)式返回 Bool 類型值,因此這里無歧義并且 return 關(guān)鍵字可以省略。
參數(shù)名簡(jiǎn)寫
Swift 自動(dòng)為內(nèi)聯(lián)閉包提供簡(jiǎn)寫參數(shù)名,可以通過 $0,$1,$2等等來直接訪問閉包參數(shù)值。
如果你在閉包表達(dá)式中使用這些簡(jiǎn)寫參數(shù)名,你可以在閉包定義中忽略對(duì)參數(shù)的定義,并且參數(shù)數(shù)量及類型會(huì)會(huì)根據(jù)函數(shù)類型自行推斷。并且 in 關(guān)鍵字也可以省略,因?yàn)殚]包表達(dá)式完全由其主體構(gòu)成:
<此處添加代碼2.7.1- 8>
這個(gè)例子中,$0,$1是閉包中第一個(gè)和第二個(gè) String 類型參數(shù)的引用。
運(yùn)算符函數(shù)
實(shí)際上還有一種更簡(jiǎn)單的方式來書寫上面的閉包表達(dá)式。Swift 中的 String 類型定義了接收兩個(gè) String 類型參數(shù)返回一個(gè) Bool 類型的的大于號(hào) (>) 函數(shù)。這完全符合 sorted 函數(shù)第二個(gè)參數(shù)所需要的函數(shù)類型。因此,你只需要簡(jiǎn)單的轉(zhuǎn)入大于號(hào),然后 Swift 會(huì)推斷出這里你需要的是字符串類型的實(shí)現(xiàn):
<此處添加代碼2.7.1- 9>
了解更多關(guān)于運(yùn)算符函數(shù)的內(nèi)容,參見 “運(yùn)算符” (Operator Functions)。
9.2 Trailing 閉包
如果你需要將一個(gè)閉包表達(dá)式傳遞給函數(shù)作為最后一個(gè)參數(shù),但是閉包表達(dá)式太長(zhǎng),這時(shí)候可以寫成 trailing 閉包來解決。Trainling 閉包是寫在函數(shù)調(diào)用括號(hào)外(之后)的閉包表達(dá)式:
<此處添加代碼2.7.2- 1>
注意:
如果函數(shù)僅接收一個(gè)閉包表達(dá)式參數(shù),并且你將閉包表達(dá)式寫為 trainling 閉包,可以不用在函數(shù)調(diào)用時(shí)添加一對(duì)圓括號(hào)。
在前面 “閉包表達(dá)式語法” 章節(jié)中的 sorted 函數(shù),其字符串排序閉包可以改寫為:
<此處添加代碼2.7.2-2>?
當(dāng)閉包長(zhǎng)到不能在一行代碼中書寫時(shí),trainling 閉包就非常有用了。比如,Swift 中的 Array 類型有一個(gè) map 函數(shù),僅接收一個(gè)閉包表達(dá)式作為參數(shù)。對(duì)于數(shù)組中的每一個(gè)元素這個(gè)閉包都執(zhí)行一次,并且返回一個(gè)與之一一對(duì)應(yīng)的值。對(duì)應(yīng)方法和返回值類型需要閉包去說明。
當(dāng)對(duì)數(shù)組中的每一個(gè)元素執(zhí)行完閉包之后,map 函數(shù)返回一個(gè)包含了所有對(duì)應(yīng)值的數(shù)組,這些值的順序與原始值在原數(shù)組中的順序相同。
下面的例子展示了怎樣使用 map 函數(shù)及 trainling 閉包來將一個(gè) Int 型數(shù)組轉(zhuǎn)換為 String 型數(shù)組。數(shù)組 [16, 58, 510] 用于生成新的數(shù)組 [“OneSix”, “FiveEight”, “FiveOneZero”]:
<此處添加代碼2.7.2-3>?
上面的代碼創(chuàng)建了一個(gè)用于將整型數(shù)字轉(zhuǎn)換為英文數(shù)組的映射字典。同時(shí)定義了一個(gè)準(zhǔn)備用于轉(zhuǎn)換的數(shù)組。
現(xiàn)在可以使用 trailing 閉包的方式傳遞一個(gè)閉包表達(dá)式給數(shù)組的 map 函數(shù),來將 numbers 類型數(shù)組轉(zhuǎn)換為 String 類型數(shù)組。需要注意的是,調(diào)用 numbers.map 函數(shù)并不需要在 map 后面添加圓括號(hào),因?yàn)?map 方法只需要一個(gè)參數(shù),并且這個(gè)參數(shù)以 trainling 閉包方式提供:
<此處添加代碼2.7.2-4>?
map 函數(shù)對(duì)數(shù)組中的每一個(gè)元素都調(diào)用了閉包表達(dá)式。 不需要指定閉包的輸入?yún)?shù)為 number 類型,Swift 能自動(dòng)根據(jù)數(shù)組元素推斷其類型。
在這個(gè)例子中,閉包的 number 參數(shù)被聲明為一個(gè)變量參數(shù),參見“常量和變量形參”(Constant and Variable Parameters),因此可以在閉包函數(shù)體內(nèi)對(duì)其進(jìn)行修改。 閉包表達(dá)式指定了返回值類型為 String,用來說明用于返回的數(shù)組類型。
閉包表達(dá)式在每次被調(diào)時(shí)創(chuàng)建一個(gè)字符串并返回。它使用求余運(yùn)算符 (number %?10) 來計(jì)算最后一位數(shù)字,并在 digitNames 字典中尋找映射字符串。這個(gè)閉包可以用來生成一個(gè)大于 0 整數(shù)的字符串代表。
注意:
訪問字典 digitNames 的下標(biāo)后跟著一個(gè)嘆號(hào) (!),因?yàn)樽值湎聵?biāo)返回一個(gè)可選值 (optional value),用來表明當(dāng) key 不存在時(shí)會(huì)查找失敗。 在上例中,number % 10 保證了得到的總是 對(duì)于 digitNames 字典的有效下標(biāo)。 因此嘆號(hào)可以用于強(qiáng)展開 (force-unwrap) 存儲(chǔ)在可選下標(biāo)項(xiàng)中的 String 類型值。
從 digitNames 字典中取回的字符串被加到 output 的前面,導(dǎo)致的結(jié)果是創(chuàng)建的字符串?dāng)?shù)組與原來的整型數(shù)組反序。(表達(dá)式 number % 10 對(duì)于值 16 返回6,對(duì)于 58 返回 8,對(duì)于 510 返回 0。)
之后 number 變量被除以10。因?yàn)樗且粋€(gè)整型,在除運(yùn)算過程中向下取整,所以 16 變成 1,58 變成 5,510 變成 51。
這個(gè)過程一直持續(xù)直到 number / 10 等于 0,這時(shí)候 output 字符串被閉包返回,然后加入到 map 函數(shù)用于返回的數(shù)組中。
上面例子中, trainling 閉包整齊地將閉包的功能緊密封裝到函數(shù)的后面,而不需要將整個(gè)閉包包裹到 map 函數(shù)的一對(duì)圓括號(hào)中。
9.3 捕獲值
一個(gè)閉包可以在其定義的上下文中捕獲常量和變量。閉包能在其主體內(nèi)引用并且修改這些常量和變量值,即使這些常量和變量在其原定義域內(nèi)已經(jīng)不存在。
在 Swift 中閉包最簡(jiǎn)單的形式是寫另一個(gè)函數(shù)主體中的嵌套函數(shù)。嵌套函數(shù)可以捕獲其包裹函數(shù)的參數(shù)以及任何在包裹函數(shù)中定義的常量和變量。
如下例子中 makeIncrementor 函數(shù)包含一個(gè)名為 incrementor 的嵌套函數(shù)。incrementor 嵌套函數(shù)從其上下文中捕獲兩個(gè)值 runningTotal 和 amount。捕獲到這兩個(gè)值后,makeIncrementor 函數(shù)返回一個(gè)閉包 incrementor,這個(gè)閉包每次被調(diào)用時(shí)會(huì)讓 runningTotal 值增加 amount:
<此處添加代碼2.7.3- 1>
makeIncrementor 函數(shù)的返回類型是 () -> Int。這表示它返回一個(gè)函數(shù),而不是一個(gè)簡(jiǎn)單的值。被返回的這個(gè)函數(shù)不接收參數(shù),并且返回一個(gè) Int 類型值。想了解更多函數(shù)如何返回其他函數(shù),參見 “作為返回類型的函數(shù)類型” 這個(gè)章節(jié)。
makeIncrementor 函數(shù)定義了一個(gè)名為 runningTotal 的整型變量,來保存用于返回的當(dāng)前計(jì)數(shù)值。這個(gè)變量的初值為 0。
makeIncrementor 函數(shù)僅有一個(gè)外部名為 forIncrement 內(nèi)部名為 amount 的 Int 類型形參。傳入的形參對(duì)應(yīng)的實(shí)參值指明了 runningTotal 每次增加多少。
makeIncrementor 定義了一個(gè)名為 incrementor 的嵌套函數(shù),有它來執(zhí)行實(shí)際的增加操作。這個(gè)函數(shù)簡(jiǎn)單的把 amount 加到 runningTotal,并返回結(jié)果。
單獨(dú)來看,incrementor 函數(shù)看起來有一些特殊:
<此處添加代碼2.7.3- 2>
incrementor 函數(shù)沒有任何形參,然而在函數(shù)體中仍然引用到 runningTotal 和 amount。它通過捕獲在上下文環(huán)境中存在的 runningTotal 和 amount 值來實(shí)現(xiàn)。
因?yàn)樗]有修改 amount 的值,increment 實(shí)際上在 amount 中存儲(chǔ)了值的拷貝。這個(gè)值著隨著新的 incrementor 函數(shù)被存儲(chǔ)。
但是,因?yàn)樗看伪徽{(diào)用的時(shí)候都修改了 runningTotal,incrmentor 捕獲了一個(gè)當(dāng)前 runningTotal 變量的引用,不僅僅只是值的拷貝。捕獲引用可以確保 makeIncrementor 函數(shù)結(jié)束時(shí) runningTotal 變量不會(huì)隨之消失,同時(shí)確保了 runningTotal 值在下一次被調(diào)用的時(shí)候仍可用。
注意:
Swift 來決定哪些值是捕獲拷貝哪些值是捕獲引用。你不需要為 amount 和 runningTotal 添加注解來說明這兩個(gè)值將用于嵌套函數(shù)中。Swift 也負(fù)責(zé)包括 runningTotal 的釋放在內(nèi)的所有內(nèi)存管理任務(wù)。
下面是一個(gè)使用 makeIncrementor 函數(shù)的例子:
<此處添加代碼2.7.3- 3>
該例子定義了一個(gè)叫做 incrementByTen 的常量,該常量指向一個(gè)每次調(diào)用會(huì)將 runningTotal 值加 10 的 incrementor 函數(shù)。多次調(diào)用這個(gè)函數(shù)的結(jié)果如下:
<此處添加代碼2.7.3- 4>
如果你創(chuàng)建了另一個(gè) incrementor,它會(huì)有自己獨(dú)立的 runningTotal 變量引用。下面的例子中,incrementBySevne 捕獲了一個(gè)新的 runningTotal 變量,該變量和 incrementByTen 中捕獲的變量沒有聯(lián)系:
<此處添加代碼2.7.3- 5>
注意:
如果將閉包分配給一個(gè)類實(shí)例的屬性, 并且指向該實(shí)例或其成員來捕獲該實(shí)例, 這會(huì)導(dǎo)致閉包和實(shí)例間的強(qiáng)循環(huán)引用。Swift 使用捕獲列表來打破這種強(qiáng)循環(huán)引用。更多信息,,參見 “強(qiáng)循環(huán)引用” (Strong ReferenceCycles for Closures)。?
9.4 閉包是引用類型
在上面的例子中,incrementBySeven 和 incrementByTen 是常量,但引用了這些常量的閉包仍然可以將他們捕捉到的變量 runningTotal 值增加。這是因?yàn)楹瘮?shù)和閉包是引用類型。
無論什么時(shí)候,當(dāng)你把函數(shù)或閉包賦值給一個(gè)常量或變量時(shí),實(shí)際上是把常量或變量設(shè)置為函數(shù)或閉包的引用。在上面的例子中,incrementByTen 是一個(gè)閉包的引用,而非閉包本身。
這也意味著,如果你將一個(gè)閉包賦值給兩個(gè)不同的常量或變量,這兩個(gè)常量或變量將指向同一個(gè)閉包:
<此處添加代碼2.7.4- 1>