閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代碼塊(blocks)以及其他一些編程語言中的匿名函數(shù)(Lambdas)比較相似。
閉包可以捕獲和存儲(chǔ)其所在上下文中任意常量和變量的引用。被稱為包裹常量和變量。 Swift 會(huì)為你管理在捕獲過程中涉及到的所有內(nèi)存操作。
注意
如果你不熟悉捕獲(capturing)這個(gè)概念也不用擔(dān)心,在 值捕獲 章節(jié)有它更詳細(xì)的介紹。
在 函數(shù) 章節(jié)中介紹的全局和嵌套函數(shù)實(shí)際上也是特殊的閉包,閉包采用如下三種形式之一:
Swift 的閉包表達(dá)式擁有簡潔的風(fēng)格,并鼓勵(lì)在常見場景中進(jìn)行語法優(yōu)化,主要優(yōu)化如下:
return
關(guān)鍵字嵌套函數(shù) 作為復(fù)雜函數(shù)的一部分時(shí),它自包含代碼塊式的定義和命名形式在使用上帶來了方便。當(dāng)然,編寫未完整聲明和沒有函數(shù)名的類函數(shù)結(jié)構(gòu)代碼是很有用的,尤其是在編碼中涉及到函數(shù)作為參數(shù)的那些方法時(shí)。
閉包表達(dá)式是一種構(gòu)建內(nèi)聯(lián)閉包的方式,它的語法簡潔。在保證不丟失它語法清晰明了的同時(shí),閉包表達(dá)式提供了幾種優(yōu)化的語法簡寫形式。下面通過對(duì) sorted(by:)
這一個(gè)案例的多次迭代改進(jìn)來展示這個(gè)過程,每次迭代都使用了更加簡明的方式描述了相同功能。。
Swift 標(biāo)準(zhǔn)庫提供了名為 sorted(by:)
的方法,它會(huì)基于你提供的排序閉包表達(dá)式的判斷結(jié)果對(duì)數(shù)組中的值(類型確定)進(jìn)行排序。一旦它完成排序過程,sorted(by:)
方法會(huì)返回一個(gè)與舊數(shù)組類型大小相同類型的新數(shù)組,該數(shù)組的元素有著正確的排序順序。原數(shù)組不會(huì)被 sorted(by:)
方法修改。
下面的閉包表達(dá)式示例使用 sorted(by:)
方法對(duì)一個(gè) String
類型的數(shù)組進(jìn)行字母逆序排序。以下是初始數(shù)組:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
sorted(by:)
方法接受一個(gè)閉包,該閉包函數(shù)需要傳入與數(shù)組元素類型相同的兩個(gè)值,并返回一個(gè)布爾類型值來表明當(dāng)排序結(jié)束后傳入的第一個(gè)參數(shù)排在第二個(gè)參數(shù)前面還是后面。如果第一個(gè)參數(shù)值出現(xiàn)在第二個(gè)參數(shù)值前面,排序閉包函數(shù)需要返回 true
,反之返回 false
。
該例子對(duì)一個(gè) String
類型的數(shù)組進(jìn)行排序,因此排序閉包函數(shù)類型需為 (String, String) -> Bool
。
提供排序閉包函數(shù)的一種方式是撰寫一個(gè)符合其類型要求的普通函數(shù),并將其作為 sorted(by:)
方法的參數(shù)傳入:
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames 為 ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一個(gè)字符串(s1
)大于第二個(gè)字符串(s2
),backward(_:_:)
函數(shù)會(huì)返回 true
,表示在新的數(shù)組中 s1
應(yīng)該出現(xiàn)在 s2
前。對(duì)于字符串中的字符來說,“大于”表示“按照字母順序較晚出現(xiàn)”。這意味著字母 "B"
大于字母 "A"
,字符串 "Tom"
大于字符串 "Tim"
。該閉包將進(jìn)行字母逆序排序,"Barry"
將會(huì)排在 "Alex"
之前。
然而,以這種方式來編寫一個(gè)實(shí)際上很簡單的表達(dá)式(a > b
),確實(shí)太過繁瑣了。對(duì)于這個(gè)例子來說,利用閉包表達(dá)式語法可以更好地構(gòu)造一個(gè)內(nèi)聯(lián)排序閉包。
閉包表達(dá)式語法有如下的一般形式:
{ (parameters) -> return type in
statements
}
閉包表達(dá)式參數(shù) 可以是 in-out 參數(shù),但不能設(shè)定默認(rèn)值。如果你命名了可變參數(shù),也可以使用此可變參數(shù)。元組也可以作為參數(shù)和返回值。
下面的例子展示了之前 backward(_:_:)
函數(shù)對(duì)應(yīng)的閉包表達(dá)式版本的代碼:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
需要注意的是內(nèi)聯(lián)閉包參數(shù)和返回值類型聲明與 backward(_:_:)
函數(shù)類型聲明相同。在這兩種方式中,都寫成了 (s1: String, s2: String) -> Bool
。然而在內(nèi)聯(lián)閉包表達(dá)式中,函數(shù)和返回值類型都寫在大括號(hào)內(nèi),而不是大括號(hào)外。
閉包的函數(shù)體部分由關(guān)鍵字 in
引入。該關(guān)鍵字表示閉包的參數(shù)和返回值類型定義已經(jīng)完成,閉包函數(shù)體即將開始。
由于這個(gè)閉包的函數(shù)體部分如此短,以至于可以將其改寫成一行代碼:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
該例中 sorted(by:)
方法的整體調(diào)用保持不變,一對(duì)圓括號(hào)仍然包裹住了方法的整個(gè)參數(shù)。然而,參數(shù)現(xiàn)在變成了內(nèi)聯(lián)閉包。
因?yàn)榕判蜷]包函數(shù)是作為 sorted(by:)
方法的參數(shù)傳入的,Swift 可以推斷其參數(shù)和返回值的類型。sorted(by:)
方法被一個(gè)字符串?dāng)?shù)組調(diào)用,因此其參數(shù)必須是 (String, String) -> Bool
類型的函數(shù)。這意味著 (String, String)
和 Bool
類型并不需要作為閉包表達(dá)式定義的一部分。因?yàn)樗械念愋投伎梢员徽_推斷,返回箭頭(->
)和圍繞在參數(shù)周圍的括號(hào)也可以被省略:
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
實(shí)際上,通過內(nèi)聯(lián)閉包表達(dá)式構(gòu)造的閉包作為參數(shù)傳遞給函數(shù)或方法時(shí),總是能夠推斷出閉包的參數(shù)和返回值類型。這意味著閉包作為函數(shù)或者方法的參數(shù)時(shí),你幾乎不需要利用完整格式構(gòu)造內(nèi)聯(lián)閉包。
盡管如此,你仍然可以明確寫出有著完整格式的閉包。如果完整格式的閉包能夠提高代碼的可讀性,則我們更鼓勵(lì)采用完整格式的閉包。而在 sorted(by:)
方法這個(gè)例子里,顯然閉包的目的就是排序。由于這個(gè)閉包是為了處理字符串?dāng)?shù)組的排序,因此讀者能夠推測出這個(gè)閉包是用于字符串處理的。
單行表達(dá)式閉包可以通過省略 return
關(guān)鍵字來隱式返回單行表達(dá)式的結(jié)果,如上版本的例子可以改寫為:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
在這個(gè)例子中,sorted(by:)
方法的參數(shù)類型明確了閉包必須返回一個(gè) Bool
類型值。因?yàn)殚]包函數(shù)體只包含了一個(gè)單一表達(dá)式(s1 > s2
),該表達(dá)式返回 Bool
類型值,因此這里沒有歧義,return
關(guān)鍵字可以省略。
Swift 自動(dòng)為內(nèi)聯(lián)閉包提供了參數(shù)名稱縮寫功能,你可以直接通過 $0
,$1
,$2
來順序調(diào)用閉包的參數(shù),以此類推。
如果你在閉包表達(dá)式中使用參數(shù)名稱縮寫,你可以在閉包定義中省略參數(shù)列表,并且對(duì)應(yīng)參數(shù)名稱縮寫的類型會(huì)通過函數(shù)類型進(jìn)行推斷。in
關(guān)鍵字也同樣可以被省略,因?yàn)榇藭r(shí)閉包表達(dá)式完全由閉包函數(shù)體構(gòu)成:
reversedNames = names.sorted(by: { $0 > $1 } )
在這個(gè)例子中,$0
和 $1
表示閉包中第一個(gè)和第二個(gè) String
類型的參數(shù)。
實(shí)際上還有一種更簡短的方式來編寫上面例子中的閉包表達(dá)式。Swift 的 String
類型定義了關(guān)于大于號(hào)(>
)的字符串實(shí)現(xiàn),其作為一個(gè)函數(shù)接受兩個(gè) String
類型的參數(shù)并返回 Bool
類型的值。而這正好與 sorted(by:)
方法的參數(shù)需要的函數(shù)類型相符合。因此,你可以簡單地傳遞一個(gè)大于號(hào),Swift 可以自動(dòng)推斷找到系統(tǒng)自帶的那個(gè)字符串函數(shù)的實(shí)現(xiàn):
reversedNames = names.sorted(by: >)
更多關(guān)于運(yùn)算符方法的內(nèi)容請(qǐng)查看 運(yùn)算符方法。
如果你需要將一個(gè)很長的閉包表達(dá)式作為最后一個(gè)參數(shù)傳遞給函數(shù),將這個(gè)閉包替換成為尾隨閉包的形式很有用。尾隨閉包是一個(gè)書寫在函數(shù)圓括號(hào)之后的閉包表達(dá)式,函數(shù)支持將其作為最后一個(gè)參數(shù)調(diào)用。在使用尾隨閉包時(shí),你不用寫出它的參數(shù)標(biāo)簽:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函數(shù)體部分
}
// 以下是不使用尾隨閉包進(jìn)行函數(shù)調(diào)用
someFunctionThatTakesAClosure(closure: {
// 閉包主體部分
})
// 以下是使用尾隨閉包進(jìn)行函數(shù)調(diào)用
someFunctionThatTakesAClosure() {
// 閉包主體部分
}
在 閉包表達(dá)式語法 上章節(jié)中的字符串排序閉包可以作為尾隨包的形式改寫在 sorted(by:)
方法圓括號(hào)的外面:
reversedNames = names.sorted() { $0 > $1 }
如果閉包表達(dá)式是函數(shù)或方法的唯一參數(shù),則當(dāng)你使用尾隨閉包時(shí),你甚至可以把 ()
省略掉:
reversedNames = names.sorted { $0 > $1 }
當(dāng)閉包非常長以至于不能在一行中進(jìn)行書寫時(shí),尾隨閉包變得非常有用。舉例來說,Swift 的 Array
類型有一個(gè) map(_:)
方法,這個(gè)方法獲取一個(gè)閉包表達(dá)式作為其唯一參數(shù)。該閉包函數(shù)會(huì)為數(shù)組中的每一個(gè)元素調(diào)用一次,并返回該元素所映射的值。具體的映射方式和返回值類型由閉包來指定。
當(dāng)提供給數(shù)組的閉包應(yīng)用于每個(gè)數(shù)組元素后,map(_:)
方法將返回一個(gè)新的數(shù)組,數(shù)組中包含了與原數(shù)組中的元素一一對(duì)應(yīng)的映射后的值。
下例介紹了如何在 map(_:)
方法中使用尾隨閉包將 Int
類型數(shù)組 [16, 58, 510]
轉(zhuǎn)換為包含對(duì)應(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)建了一個(gè)整型數(shù)位和它們英文版本名字相映射的字典。同時(shí)還定義了一個(gè)準(zhǔn)備轉(zhuǎn)換為字符串?dāng)?shù)組的整型數(shù)組。
你現(xiàn)在可以通過傳遞一個(gè)尾隨閉包給 numbers
數(shù)組的 map(_:)
方法來創(chuàng)建對(duì)應(yīng)的字符串版本數(shù)組:
let strings = numbers.map {
(number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings 常量被推斷為字符串類型數(shù)組,即 [String]
// 其值為 ["OneSix", "FiveEight", "FiveOneZero"]
map(_:)
為數(shù)組中每一個(gè)元素調(diào)用了一次閉包表達(dá)式。你不需要指定閉包的輸入?yún)?shù) number
的類型,因?yàn)榭梢酝ㄟ^要映射的數(shù)組類型進(jìn)行推斷。
在該例中,局部變量 number
的值由閉包中的 number
參數(shù)獲得,因此可以在閉包函數(shù)體內(nèi)對(duì)其進(jìn)行修改,(閉包或者函數(shù)的參數(shù)總是常量),閉包表達(dá)式指定了返回類型為 String
,以表明存儲(chǔ)映射值的新數(shù)組類型為 String
。
閉包表達(dá)式在每次被調(diào)用的時(shí)候創(chuàng)建了一個(gè)叫做 output
的字符串并返回。其使用求余運(yùn)算符(number % 10
)計(jì)算最后一位數(shù)字并利用 digitNames
字典獲取所映射的字符串。這個(gè)閉包能夠用于創(chuàng)建任意正整數(shù)的字符串表示。
注意
字典
digitNames
下標(biāo)后跟著一個(gè)嘆號(hào)(!
),因?yàn)樽值湎聵?biāo)返回一個(gè)可選值(optional value),表明該鍵不存在時(shí)會(huì)查找失敗。在上例中,由于可以確定number % 10
總是digitNames
字典的有效下標(biāo),因此嘆號(hào)可以用于強(qiáng)制解包(force-unwrap)存儲(chǔ)在下標(biāo)的可選類型的返回值中的String
類型的值。
從 digitNames
字典中獲取的字符串被添加到 output
的前部,逆序建立了一個(gè)字符串版本的數(shù)字。(在表達(dá)式 number % 10
中,如果 number
為 16
,則返回 6
,58
返回 8
,510
返回 0
。)
number
變量之后除以 10
。因?yàn)槠涫钦麛?shù),在計(jì)算過程中未除盡部分被忽略。因此 16
變成了 1
,58
變成了 5
,510
變成了 51
。
整個(gè)過程重復(fù)進(jìn)行,直到 number /= 10
為 0
,這時(shí)閉包會(huì)將字符串 output
返回,而 map(_:)
方法則會(huì)將字符串添加到映射數(shù)組中。
在上面的例子中,通過尾隨閉包語法,優(yōu)雅地在函數(shù)后封裝了閉包的具體功能,而不再需要將整個(gè)閉包包裹在 map(_:)
方法的括號(hào)內(nèi)。
閉包可以在其被定義的上下文中捕獲常量或變量。即使定義這些常量和變量的原作用域已經(jīng)不存在,閉包仍然可以在閉包函數(shù)體內(nèi)引用和修改這些值。
Swift 中,可以捕獲值的閉包的最簡單形式是嵌套函數(shù),也就是定義在其他函數(shù)的函數(shù)體內(nèi)的函數(shù)。嵌套函數(shù)可以捕獲其外部函數(shù)所有的參數(shù)以及定義的常量和變量。
舉個(gè)例子,這有一個(gè)叫做 makeIncrementer
的函數(shù),其包含了一個(gè)叫做 incrementer
的嵌套函數(shù)。嵌套函數(shù) incrementer()
從上下文中捕獲了兩個(gè)值,runningTotal
和 amount
。捕獲這些值之后,makeIncrementer
將 incrementer
作為閉包返回。每次調(diào)用 incrementer
時(shí),其會(huì)以 amount
作為增量增加 runningTotal
的值。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer
返回類型為 () -> Int
。這意味著其返回的是一個(gè)函數(shù),而非一個(gè)簡單類型的值。該函數(shù)在每次調(diào)用時(shí)不接受參數(shù),只返回一個(gè) Int
類型的值。關(guān)于函數(shù)返回其他函數(shù)的內(nèi)容,請(qǐng)查看 函數(shù)類型作為返回類型。
makeIncrementer(forIncrement:)
函數(shù)定義了一個(gè)初始值為 0
的整型變量 runningTotal
,用來存儲(chǔ)當(dāng)前總計(jì)數(shù)值。該值為 incrementer
的返回值。
makeIncrementer(forIncrement:)
有一個(gè) Int
類型的參數(shù),其外部參數(shù)名為 forIncrement
,內(nèi)部參數(shù)名為 amount
,該參數(shù)表示每次 incrementer
被調(diào)用時(shí) runningTotal
將要增加的量。makeIncrementer
函數(shù)還定義了一個(gè)嵌套函數(shù) incrementer
,用來執(zhí)行實(shí)際的增加操作。該函數(shù)簡單地使 runningTotal
增加 amount
,并將其返回。
如果我們單獨(dú)考慮嵌套函數(shù) incrementer()
,會(huì)發(fā)現(xiàn)它有些不同尋常:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer()
函數(shù)并沒有任何參數(shù),但是在函數(shù)體內(nèi)訪問了 runningTotal
和 amount
變量。這是因?yàn)樗鼜耐鈬瘮?shù)捕獲了 runningTotal
和 amount
變量的引用。捕獲引用保證了 runningTotal
和 amount
變量在調(diào)用完 makeIncrementer
后不會(huì)消失,并且保證了在下一次執(zhí)行 incrementer
函數(shù)時(shí),runningTotal
依舊存在。
注意
為了優(yōu)化,如果一個(gè)值不會(huì)被閉包改變,或者在閉包創(chuàng)建后不會(huì)改變,Swift 可能會(huì)改為捕獲并保存一份對(duì)值的拷貝。
Swift 也會(huì)負(fù)責(zé)被捕獲變量的所有內(nèi)存管理工作,包括釋放不再需要的變量。
下面是一個(gè)使用 makeIncrementer
的例子:
let incrementByTen = makeIncrementer(forIncrement: 10)
該例子定義了一個(gè)叫做 incrementByTen
的常量,該常量指向一個(gè)每次調(diào)用會(huì)將其 runningTotal
變量增加 10
的 incrementer
函數(shù)。調(diào)用這個(gè)函數(shù)多次可以得到以下結(jié)果:
incrementByTen()
// 返回的值為10
incrementByTen()
// 返回的值為20
incrementByTen()
// 返回的值為30
如果你創(chuàng)建了另一個(gè) incrementer
,它會(huì)有屬于自己的引用,指向一個(gè)全新、獨(dú)立的 runningTotal
變量:
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回的值為7
再次調(diào)用原來的 incrementByTen
會(huì)繼續(xù)增加它自己的 runningTotal
變量,該變量和 incrementBySeven
中捕獲的變量沒有任何聯(lián)系:
incrementByTen()
// 返回的值為40
注意
如果你將閉包賦值給一個(gè)類實(shí)例的屬性,并且該閉包通過訪問該實(shí)例或其成員而捕獲了該實(shí)例,你將在閉包和該實(shí)例間創(chuàng)建一個(gè)循環(huán)強(qiáng)引用。Swift 使用捕獲列表來打破這種循環(huán)強(qiáng)引用。更多信息,請(qǐng)參考 閉包引起的循環(huán)強(qiáng)引用。
上面的例子中,incrementBySeven
和 incrementByTen
都是常量,但是這些常量指向的閉包仍然可以增加其捕獲的變量的值。這是因?yàn)楹瘮?shù)和閉包都是引用類型。
無論你將函數(shù)或閉包賦值給一個(gè)常量還是變量,你實(shí)際上都是將常量或變量的值設(shè)置為對(duì)應(yīng)函數(shù)或閉包的引用。上面的例子中,指向閉包的引用 incrementByTen
是一個(gè)常量,而并非閉包內(nèi)容本身。
這也意味著如果你將閉包賦值給了兩個(gè)不同的常量或變量,兩個(gè)值都會(huì)指向同一個(gè)閉包:
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值為50
當(dāng)一個(gè)閉包作為參數(shù)傳到一個(gè)函數(shù)中,但是這個(gè)閉包在函數(shù)返回之后才被執(zhí)行,我們稱該閉包從函數(shù)中逃逸。當(dāng)你定義接受閉包作為參數(shù)的函數(shù)時(shí),你可以在參數(shù)名之前標(biāo)注 @escaping
,用來指明這個(gè)閉包是允許“逃逸”出這個(gè)函數(shù)的。
一種能使閉包“逃逸”出函數(shù)的方法是,將這個(gè)閉包保存在一個(gè)函數(shù)外部定義的變量中。舉個(gè)例子,很多啟動(dòng)異步操作的函數(shù)接受一個(gè)閉包參數(shù)作為 completion handler。這類函數(shù)會(huì)在異步操作開始之后立刻返回,但是閉包直到異步操作結(jié)束后才會(huì)被調(diào)用。在這種情況下,閉包需要“逃逸”出函數(shù),因?yàn)殚]包需要在函數(shù)返回之后被調(diào)用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)
函數(shù)接受一個(gè)閉包作為參數(shù),該閉包被添加到一個(gè)函數(shù)外定義的數(shù)組中。如果你不將這個(gè)參數(shù)標(biāo)記為 @escaping
,就會(huì)得到一個(gè)編譯錯(cuò)誤。
將一個(gè)閉包標(biāo)記為 @escaping
意味著你必須在閉包中顯式地引用 self
。比如說,在下面的代碼中,傳遞到 someFunctionWithEscapingClosure(_:)
中的閉包是一個(gè)逃逸閉包,這意味著它需要顯式地引用 self
。相對(duì)的,傳遞到 someFunctionWithNonescapingClosure(_:)
中的閉包是一個(gè)非逃逸閉包,這意味著它可以隱式引用 self
。
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出“200”
completionHandlers.first?()
print(instance.x)
// 打印出“100”
自動(dòng)閉包是一種自動(dòng)創(chuàng)建的閉包,用于包裝傳遞給函數(shù)作為參數(shù)的表達(dá)式。這種閉包不接受任何參數(shù),當(dāng)它被調(diào)用的時(shí)候,會(huì)返回被包裝在其中的表達(dá)式的值。這種便利語法讓你能夠省略閉包的花括號(hào),用一個(gè)普通的表達(dá)式來代替顯式的閉包。
我們經(jīng)常會(huì)調(diào)用采用自動(dòng)閉包的函數(shù),但是很少去實(shí)現(xiàn)這樣的函數(shù)。舉個(gè)例子來說,assert(condition:message:file:line:)
函數(shù)接受自動(dòng)閉包作為它的 condition
參數(shù)和 message
參數(shù);它的 condition
參數(shù)僅會(huì)在 debug 模式下被求值,它的 message
參數(shù)僅當(dāng) condition
參數(shù)為 false
時(shí)被計(jì)算求值。
自動(dòng)閉包讓你能夠延遲求值,因?yàn)橹钡侥阏{(diào)用這個(gè)閉包,代碼段才會(huì)被執(zhí)行。延遲求值對(duì)于那些有副作用(Side Effect)和高計(jì)算成本的代碼來說是很有益處的,因?yàn)樗沟媚隳芸刂拼a的執(zhí)行時(shí)機(jī)。下面的代碼展示了閉包如何延時(shí)求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出“5”
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出“5”
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出“4”
盡管在閉包的代碼中,customersInLine
的第一個(gè)元素被移除了,不過在閉包被調(diào)用之前,這個(gè)元素是不會(huì)被移除的。如果這個(gè)閉包永遠(yuǎn)不被調(diào)用,那么在閉包里面的表達(dá)式將永遠(yuǎn)不會(huì)執(zhí)行,那意味著列表中的元素永遠(yuǎn)不會(huì)被移除。請(qǐng)注意,customerProvider
的類型不是 String
,而是 () -> String
,一個(gè)沒有參數(shù)且返回值為 String
的函數(shù)。
將閉包作為參數(shù)傳遞給函數(shù)時(shí),你能獲得同樣的延時(shí)求值行為。
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出“Now serving Alex!”
上面的 serve(customer:)
函數(shù)接受一個(gè)返回顧客名字的顯式的閉包。下面這個(gè)版本的 serve(customer:)
完成了相同的操作,不過它并沒有接受一個(gè)顯式的閉包,而是通過將參數(shù)標(biāo)記為 @autoclosure
來接收一個(gè)自動(dòng)閉包?,F(xiàn)在你可以將該函數(shù)當(dāng)作接受 String
類型參數(shù)(而非閉包)的函數(shù)來調(diào)用。customerProvider
參數(shù)將自動(dòng)轉(zhuǎn)化為一個(gè)閉包,因?yàn)樵搮?shù)被標(biāo)記了 @autoclosure
特性。
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印“Now serving Ewa!”
注意
過度使用
autoclosures
會(huì)讓你的代碼變得難以理解。上下文和函數(shù)名應(yīng)該能夠清晰地表明求值是被延遲執(zhí)行的。
如果你想讓一個(gè)自動(dòng)閉包可以“逃逸”,則應(yīng)該同時(shí)使用 @autoclosure
和 @escaping
屬性。@escaping
屬性的講解見上面的 逃逸閉包。
// customersInLine i= ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))
print("Collected \(customerProviders.count) closures.")
// 打印“Collected 2 closures.”
for customerProvider in customerProviders {
print("Now serving \(customerProvider())!")
}
// 打印“Now serving Barry!”
// 打印“Now serving Daniella!”
在上面的代碼中,collectCustomerProviders(_:)
函數(shù)并沒有調(diào)用傳入的 customerProvider
閉包,而是將閉包追加到了 customerProviders
數(shù)組中。這個(gè)數(shù)組定義在函數(shù)作用域范圍外,這意味著數(shù)組內(nèi)的閉包能夠在函數(shù)返回之后被調(diào)用。因此,customerProvider
參數(shù)必須允許“逃逸”出函數(shù)作用域。