閉包是功能性自包含模塊粗井,可以在代碼中被傳遞和使用尔破。Swift中的閉包與 C 和 Objective-C中的 blocks 以及其他一些編程語言中的 lambdas 比較相似。
閉包可以?捕獲?和存儲其所在上下文中任意常量和變量的引用浇衬。 這就是所謂的閉合并包裹著這些常量和變量懒构,俗稱閉包。Swift會為您管理在?捕獲?過程中涉及到的內(nèi)存操作径玖。
注意:如果您不熟悉?捕獲?(capturing) 這個概念也不用擔(dān)心痴脾,后面會詳細對其進行介紹。
在Swift函數(shù)章節(jié)中介紹的全局和嵌套函數(shù)實際上也是特殊的閉包梳星,閉包采取如下三種形式之一:
全局函數(shù)是一個有名字但不會捕獲任何值的閉包
嵌套函數(shù)是一個有名字并可以捕獲其封閉函數(shù)域內(nèi)值的閉包
閉包表達式是一個利用輕量級語法所寫的可以捕獲其上下文中變量或常量值的沒有名字的閉包
Swift的閉包表達式擁有簡潔的風(fēng)格赞赖,并鼓勵在常見場景中以實現(xiàn)語法優(yōu)化滚朵,主要優(yōu)化如下:
利用上下文推斷參數(shù)和返回值類型
單表達式(single-expression)閉包可以省略?return?關(guān)鍵字
參數(shù)名稱簡寫
Trailing 閉包語法
閉包表達式
嵌套函數(shù)是一種在較復(fù)雜函數(shù)中方便進行命名和定義自包含代碼模塊的方式。 當(dāng)然前域,有時候撰寫小巧的沒有完整定義和命名的類函數(shù)結(jié)構(gòu)也是很有用處的辕近,尤其是在處理一些函數(shù)并需要將另外一些函數(shù)作為該函數(shù)的參數(shù)時。
閉包表達式是一種利用簡潔語法構(gòu)建內(nèi)聯(lián)閉包的方式匿垄。 閉包表達式提供了一些語法優(yōu)化移宅,使得撰寫閉包變得簡單明了。 下面閉包表達式的例子通過使用幾次迭代展示了?sort?函數(shù)定義和語法優(yōu)化的方式椿疗。 每一次迭代都用更簡潔的方式描述了相同的功能漏峰。
sort?函數(shù)
Swift 標(biāo)準(zhǔn)庫提供了?sort?函數(shù),會根據(jù)您提供的排序閉包將已知類型數(shù)組中的值進行排序届榄。 一旦排序完成浅乔,函數(shù)會返回一個與原數(shù)組大小相同的新數(shù)組,該數(shù)組中包含已經(jīng)正確排序的同類型元素铝条。
下面的閉包表達式示例使用?sort?函數(shù)對一個?String?類型的數(shù)組進行字母逆序排序靖苇,以下是初始數(shù)組值:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
排序函數(shù)有兩個參數(shù):
已知類型值的數(shù)組。
一個閉包班缰,采用相同類型的數(shù)組的內(nèi)容的兩個參數(shù)贤壁,并返回一個布爾值來表示是否將第一個值在排序時放到第二個值的前面或是后面。如果第一個值應(yīng)該出現(xiàn)第二個值之前埠忘,閉包需要返回true脾拆,否則返回false。
該例子對一個?String?類型的數(shù)組進行排序给梅,因此排序閉包需為?(String, String) -> Bool?類型的函數(shù)假丧。
提供排序閉包的一種方式是撰寫一個符合其類型要求的普通函數(shù)双揪,并將其作為?sort?函數(shù)的第二個參數(shù)傳入:
func backwards(s1: String, s2: String) -> Bool {
return s1 > s2
}
var reversed = sort(names, backwards)
// reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一個字符串 (s1) 大于第二個字符串 (s2),backwards?函數(shù)則返回?true,表示在新的數(shù)組中?s1?應(yīng)該出現(xiàn)在?s2?前所计。 字符中的 "大于" 表示 "按照字母順序后出現(xiàn)"唾琼。 這意味著字母 "B" 大于字母 "A", 字符串 "Tom" 大于字符串 "Tim"。 其將進行字母逆序排序疯趟,"Barry" 將會排在 "Alex" 之后,一次類推拘哨。
然而,這是一個相當(dāng)冗長的方式信峻,本質(zhì)上只是寫了一個單表達式函數(shù) (a > b)倦青。 在下面的例子中,利用閉合表達式語法可以更好的構(gòu)造一個內(nèi)聯(lián)排序閉包盹舞。
閉包表達式語法
閉包表達式語法有如下一般形式:
{ (parameters) -> returnType in
statements
}
閉包表達式語法可以使用常量产镐、變量和?inout?類型作為參數(shù)隘庄,但不提供默認(rèn)值。 也可以在參數(shù)列表的最后使用可變參數(shù)癣亚。元組也可以作為參數(shù)和返回值丑掺。
下面的例子展示了之前?backwards?函數(shù)對應(yīng)的閉包表達式版本的代碼:
reversed = sort(names, { (s1: String, s2: String) -> Bool in
return s1 > s2
})
需要注意的是內(nèi)聯(lián)閉包參數(shù)和返回值類型聲明與?backwards?函數(shù)類型聲明相同。 在這兩種方式中述雾,都寫成了 (s1: String, s2: String) -> Bool類型街州。 然而在內(nèi)聯(lián)閉包表達式中,函數(shù)和返回值類型都寫在大括號內(nèi)玻孟,而不是大括號外唆缴。
閉包的函數(shù)體部分由關(guān)鍵字?in?引入。 該關(guān)鍵字表示閉包的參數(shù)和返回值類型定義已經(jīng)完成黍翎,閉包函數(shù)體即將開始琐谤。
因為這個閉包的函數(shù)體部分如此短以至于可以將其改寫成一行代碼:
reversed = sort(names, { (s1: String, s2: String) -> Bool in return s1 > s2 } )
這說明?sort?函數(shù)的整體調(diào)用保持不變,一對圓括號仍然包裹住了函數(shù)中整個參數(shù)集合玩敏。而其中一個參數(shù)現(xiàn)在變成了內(nèi)聯(lián)閉包 (相比于?backwards?版本的代碼)斗忌。
根據(jù)上下文推斷類型
因為排序閉包是作為函數(shù)的參數(shù)進行傳入的,Swift可以推斷其參數(shù)和返回值的類型旺聚。?sort?期望第二個參數(shù)是類型為?(String, String) -> Bool?的函數(shù)织阳,因此實際上?String,?String?和?Bool?類型并不需要作為閉包表達式定義中的一部分。 因為所有的類型都可以被正確推斷砰粹,返回箭頭 (->) 和 圍繞在參數(shù)周圍的括號也可以被省略:
reversed = sort(names, { s1, s2 in return s1 > s2 } )
實際上任何情況下唧躲,通過內(nèi)聯(lián)閉包表達式構(gòu)造的閉包作為參數(shù)傳遞給函數(shù)時,都可以推斷出閉包的參數(shù)和返回值類型碱璃,這意味著您幾乎不需要利用完整格式構(gòu)造任何內(nèi)聯(lián)閉包弄痹。
然而,你也可以使用明確的類型嵌器,如果你想它避免讀者閱讀可能存在的歧義肛真,這樣還是值得鼓勵的。這個排序函數(shù)例子爽航,閉包的目的是很明確的蚓让,即排序被替換,而且對讀者來說可以安全的假設(shè)閉包可能會使用字符串值,因為它正協(xié)助一個字符串?dāng)?shù)組進行排序讥珍。
單行表達式閉包可以省略?return
單行表達式閉包可以通過隱藏?return?關(guān)鍵字來隱式返回單行表達式的結(jié)果历极,如上版本的例子可以改寫為:
reversed = sort(names, { s1, s2 in s1 > s2 } )
在這個例子中,sort?函數(shù)的第二個參數(shù)函數(shù)類型明確了閉包必須返回一個?Bool?類型值衷佃。 因為閉包函數(shù)體只包含了一個單一表達式 (s1 > s2)趟卸,該表達式返回?Bool?類型值,因此這里沒有歧義,return關(guān)鍵字可以省略锄列。
參數(shù)名簡寫
Swift?自動為內(nèi)聯(lián)函數(shù)提供了參數(shù)名稱簡寫功能新蟆,您可以直接通過?$0,$1,$2等名字來引用的閉包的參數(shù)的值。
如果您在閉包表達式中使用參數(shù)名稱簡寫右蕊,您可以在閉包參數(shù)列表中省略對其的定義琼稻,并且對應(yīng)參數(shù)名稱簡寫的類型會通過函數(shù)類型進行推斷。?in?關(guān)鍵字也同樣可以被省略饶囚,因為此時閉包表達式完全由閉包函數(shù)體構(gòu)成:
reversed = sort(names, { $0 > $1 } )
在這個例子中帕翻,$0?和?$1?表示閉包中第一個和第二個?String?類型的參數(shù)。
運算符函數(shù)
實際上還有一種更簡短的方式來撰寫上面例子中的閉包表達式萝风。?Swift的?String?類型定義了關(guān)于大于號 (>) 的字符串實現(xiàn)嘀掸,讓其作為一個函數(shù)接受兩個?String?類型的參數(shù)并返回?Bool?類型的值。 而這正好與?sort?函數(shù)的第二個參數(shù)需要的函數(shù)類型相符合规惰。 因此睬塌,您可以簡單地傳遞一個大于號,Swift可以自動推斷出您想使用大于號的字符串函數(shù)實現(xiàn):
reversed = sort(names, >)
更多關(guān)于運算符表達式的內(nèi)容請查看Operator Functions歇万。
Trailing 閉包
如果您需要將一個很長的閉包表達式作為最后一個參數(shù)傳遞給函數(shù)揩晴,可以使用 trailing 閉包來增強函數(shù)的可讀性。
Trailing 閉包是一個書寫在函數(shù)括號之外(之后)的閉包表達式贪磺,函數(shù)支持將其作為最后一個參數(shù)調(diào)用硫兰。
func someFunctionThatTakesAClosure(closure: () -> ()) {
// 函數(shù)體部分
}
// 以下是不使用 trailing 閉包進行函數(shù)調(diào)用
someFunctionThatTakesAClosure({
// 閉包主體部分
})
// 以下是使用 trailing 閉包進行函數(shù)調(diào)用
someFunctionThatTakesAClosure() {
// 閉包主體部分
}
注意:如果函數(shù)只需要閉包表達式一個參數(shù),當(dāng)您使用 trailing 閉包時寒锚,您甚至可以把 () 省略掉劫映。 NOTE
在上例中作為?sort?函數(shù)參數(shù)的字符串排序閉包可以改寫為:
reversed = sort(names) { $0 > $1 }
當(dāng)閉包非常長以至于不能在一行中進行書寫時,Trailing 閉包就變得非常有用刹前。 舉例來說泳赋,Swift?的?Array?類型有一個?map?方法,其獲取一個閉包表達式作為其唯一參數(shù)喇喉。 數(shù)組中的每一個元素調(diào)用一次該閉包函數(shù)祖今,并返回該元素所映射的值(也可以是不同類型的值)。 具體的映射方式和返回值類型由閉包來指定轧飞。
當(dāng)提供給數(shù)組閉包函數(shù)后衅鹿,map?方法將返回一個新的數(shù)組,數(shù)組中包含了與原數(shù)組一一對應(yīng)的映射后的值过咬。
下例介紹了如何在?map?方法中使用 trailing 閉包將?Int?類型數(shù)組?[16,58,510]?轉(zhuǎn)換為包含對應(yīng)?String?類型的數(shù)組?["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]
上面的代碼創(chuàng)建了整數(shù)數(shù)字到他們的英文名字之間映射字典。 同時定義了一個準(zhǔn)備轉(zhuǎn)換為字符串的整型數(shù)組制妄。
您現(xiàn)在可以通過傳遞一個 trailing 閉包給?numbers?的?map?方法來創(chuàng)建對應(yīng)的字符串版本數(shù)組掸绞。 需要注意的時調(diào)用?numbers.map不需要在?map?后面包含任何括號,因為只需要傳遞閉包表達式這一個參數(shù),并且該閉包表達式參數(shù)通過 trailing 方式進行撰寫:
let strings = numbers.map {
(var number) -> String in
var output = ""
while number > 0 {
output = digitNames[number % 10]! + output
number /= 10
}
return output
}
// strings 常量被推斷為字符串類型數(shù)組衔掸,即 String[]
// 其值為 ["OneSix", "FiveEight", "FiveOneZero"]
map?在數(shù)組中為每一個元素調(diào)用了閉包表達式烫幕。 您不需要指定閉包的輸入?yún)?shù)?number?的類型,因為可以通過要映射的數(shù)組類型進行推斷敞映。
閉包?number?參數(shù)被聲明為一個變量參數(shù) (變量的具體描述請參看Constant and Variable Parameters)较曼,因此可以在閉包函數(shù)體內(nèi)對其進行修改。 閉包表達式制定了返回值類型為?String振愿,以表明存儲映射值的新數(shù)組類型為?String捷犹。
閉包表達式在每次被調(diào)用的時候創(chuàng)建了一個字符串并返回。 其使用求余運算符?(number % 10)?計算最后一位數(shù)字并利用digitNames?字典獲取所映射的字符串冕末。
注意:字典?digitNames?下標(biāo)后跟著一個嘆號 (!)萍歉,因為字典下標(biāo)返回一個可選值 (optional value),表明即使該?key不存在也不會查找失敗档桃。 在上例中枪孩,它保證了?number % 10?可以總是作為一個?digitNames?字典的有效下標(biāo)?key。 因此嘆號可以用于強展開?(force-unwrap)?存儲在可選下標(biāo)項中的?String?類型值藻肄。
從?digitNames?字典中獲取的字符串被添加到輸出的前部蔑舞,逆序建立了一個字符串版本的數(shù)字。 (在表達式?number % 10中嘹屯,如果number為16斗幼,則返回6,58返回8抚垄,510返回0)蜕窿。
number?變量之后除以10。 因為其是整數(shù)呆馁,在計算過程中未除盡部分被忽略桐经。 因此 16變成了1,58變成了5浙滤,510變成了51阴挣。
整個過程重復(fù)進行,直到?number /= 10?為0纺腊,這時閉包會將字符串輸出畔咧,而map函數(shù)則會將字符串添加到所映射的數(shù)組中。
上例中 trailing 閉包語法在函數(shù)后整潔封裝了具體的閉包功能揖膜,而不再需要將整個閉包包裹在?map?函數(shù)的括號內(nèi)誓沸。
捕獲 (Caputure)
閉包可以在其定義的上下文中捕獲常量或變量。 即使定義這些常量和變量的原作用域已經(jīng)不存在壹粟,閉包仍然可以在閉包函數(shù)體內(nèi)引用和修改這些值拜隧。
Swift最簡單的閉包形式是嵌套函數(shù),也就是定義在其他函數(shù)體內(nèi)的函數(shù)。 嵌套函數(shù)可以捕獲其外部函數(shù)所有的參數(shù)以及定義的常量和變量洪添。
下例為一個叫做?makeIncrementor?的函數(shù)垦页,其包含了一個叫做?incrementor?嵌套函數(shù)。 嵌套函數(shù)?incrementor?從上下文中捕獲了兩個值干奢,runningTotal?和?amount痊焊。 之后?makeIncrementor?將?incrementor?作為閉包返回。 每次調(diào)用?incrementor?時忿峻,其會以?amount?作為增量增加?runningTotal?的值薄啥。
func makeIncrementor(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
return incrementor
}
makeIncrementor?返回類型為?() -> Int。 這意味著其返回的是一個函數(shù)炭菌,而不是一個簡單類型值罪佳。 該函數(shù)在每次調(diào)用時不接受參數(shù)只返回一個?Int?類型的值。 關(guān)于函數(shù)返回其他函數(shù)的內(nèi)容黑低,請查看Function Types as Return Types赘艳。
makeIncrementor?函數(shù)定義了一個整型變量?runningTotal?(初始為0) 用來存儲當(dāng)前增加總數(shù)。 該值通過?incrementor?返回克握。
makeIncrementor?有一個?Int?類型的參數(shù)蕾管,其外部命名為?forIncrement, 內(nèi)部命名為?amount菩暗,表示每次?incrementor?被調(diào)用時runningTotal?將要增加的量掰曾。
incrementor?函數(shù)用來執(zhí)行實際的增加操作。 該函數(shù)簡單地使?runningTotal?增加?amount停团,并將其返回旷坦。
如果我們單獨看這個函數(shù),會發(fā)現(xiàn)看上去不同尋常:
func incrementor() -> Int {
runningTotal += amount
return runningTotal
}
incrementor?函數(shù)并沒有獲取任何參數(shù)佑稠,但是在函數(shù)體內(nèi)訪問了?runningTotal?和?amount?變量秒梅。這是因為其通過捕獲在包含它的函數(shù)體內(nèi)已經(jīng)存在的?runningTotal?和?amount?變量而實現(xiàn)。
由于沒有修改?amount?變量舌胶,incrementor?實際上捕獲并存儲了該變量的一個副本捆蜀,而該副本隨著?incrementor?一同被存儲。
然而幔嫂,因為每次調(diào)用該函數(shù)的時候都會修改?runningTotal?的值辆它,incrementor?捕獲了當(dāng)前?runningTotal?變量的引用,而不是僅僅復(fù)制該變量的初始值履恩。捕獲一個引用保證了當(dāng)?makeIncrementor?結(jié)束時候并不會消失锰茉,也保證了當(dāng)下一次執(zhí)行?incrementor?函數(shù)時,runningTotal?可以繼續(xù)增加似袁。
注意:Swift?會決定捕獲引用還是拷貝值洞辣。 您不需要標(biāo)注?amount?或者?runningTotal?來聲明在嵌入的?incrementor?函數(shù)中的使用方式咐刨。?Swift?同時也處理?runingTotal?變量的內(nèi)存管理操作昙衅,如果不再被?incrementor?函數(shù)使用扬霜,則會被清除。
下面為一個使用?makeIncrementor?的例子:
let incrementByTen = makeIncrementor(forIncrement: 10)
該例子定義了一個叫做?incrementByTen?的常量而涉,該常量指向一個每次調(diào)用會加10的?incrementor?函數(shù)著瓶。 調(diào)用這個函數(shù)多次可以得到以下結(jié)果:
incrementByTen()
// 返回的值為10
incrementByTen()
// 返回的值為20
incrementByTen()
// 返回的值為30
如果您創(chuàng)建了另一個?incrementor,其會有一個屬于自己的獨立的?runningTotal?變量的引用啼县。 下面的例子中材原,incrementBySevne?捕獲了一個新的?runningTotal?變量,該變量和?incrementByTen?中捕獲的變量沒有任何聯(lián)系:
let incrementBySeven = makeIncrementor(forIncrement: 7)
incrementBySeven()
// 返回的值為7
incrementByTen()
// 返回的值為40
注意:如果您閉包分配給一個類實例的屬性季眷,并且該閉包通過指向該實例或其成員來捕獲了該實例余蟹,您將創(chuàng)建一個在閉包和實例間的強引用環(huán)。?Swift?使用捕獲列表來打破這種強引用環(huán)子刮。更多信息威酒,請參考Strong Reference Cycles for Closures。
閉包是引用類型
上面的例子中挺峡,incrementBySeven?和?incrementByTen?是常量葵孤,但是這些常量指向的閉包仍然可以增加其捕獲的變量值。 這是因為函數(shù)和閉包都是引用類型橱赠。
無論您將函數(shù)/閉包賦值給一個常量還是變量尤仍,您實際上都是將常量/變量的值設(shè)置為對應(yīng)函數(shù)/閉包的引用。 上面的例子中狭姨,incrementByTen?指向閉包的引用是一個常量宰啦,而并非閉包內(nèi)容本身。
這也意味著如果您將閉包賦值給了兩個不同的常量/變量饼拍,兩個值都會指向同一個閉包:
純文本復(fù)制
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值為50