iOS 學(xué)習(xí) Swift 教程- 2.7 閉包(Closures)

前言:
本篇文章的目的,在于記錄學(xué)習(xí)過程,敦促自己,方便查看晒夹。

練習(xí)工具:Playground
學(xué)習(xí)網(wǎng)站: swift51

本頁內(nèi)容包括:

閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用霜第。Swift 中的閉包與 C 和 Objective-C 中的代碼塊(blocks)以及其他一些編程語言中的匿名函數(shù)比較相似。

閉包可以捕獲和存儲(chǔ)其所在上下文中任意常量和變量的引用户辞。被稱為包裹常量和變量泌类。 Swift 會(huì)為你管理在捕獲過程中涉及到的所有內(nèi)存操作。

注意 如果你不熟悉捕獲(capturing)這個(gè)概念也不用擔(dān)心底燎,你可以在值捕獲章節(jié)對其進(jìn)行詳細(xì)了解刃榨。

函數(shù)章節(jié)中介紹的全局和嵌套函數(shù)實(shí)際上也是特殊的閉包,閉包采取如下三種形式之一:

  • 全局函數(shù)是一個(gè)有名字但不會(huì)捕獲任何值的閉包
  • 嵌套函數(shù)是一個(gè)有名字并可以捕獲其封閉函數(shù)域內(nèi)值的閉包
  • 閉包表達(dá)式是一個(gè)利用輕量級語法所寫的可以捕獲其上下文中變量或常量值的匿名閉包

Swift 的閉包表達(dá)式擁有簡潔的風(fēng)格双仍,并鼓勵(lì)在常見場景中進(jìn)行語法優(yōu)化枢希,主要優(yōu)化如下:

  • 利用上下文推斷參數(shù)和返回值類型
  • 隱式返回單表達(dá)式閉包,即單表達(dá)式閉包可以省略 return 關(guān)鍵字
  • 參數(shù)名稱縮寫
  • 尾隨閉包語法

閉包表達(dá)式

嵌套函數(shù)是一個(gè)在較復(fù)雜函數(shù)中方便進(jìn)行命名和定義自包含代碼模塊的方式朱沃。當(dāng)然晴玖,有時(shí)候編寫小巧的沒有完整定義和命名的類函數(shù)結(jié)構(gòu)也是很有用處的,尤其是在你處理一些函數(shù)并需要將另外一些函數(shù)作為該函數(shù)的參數(shù)時(shí)。

閉包表達(dá)式是一種利用簡潔語法構(gòu)建內(nèi)聯(lián)閉包的方式呕屎。閉包表達(dá)式提供了一些語法優(yōu)化,使得撰寫閉包變得簡單明了敬察。下面閉包表達(dá)式的例子通過使用幾次迭代展示了 sorted(by:) 方法定義和語法優(yōu)化的方式秀睛。每一次迭代都用更簡潔的方式描述了相同的功能。


sorted 方法

Swift 標(biāo)準(zhǔn)庫提供了名為 sorted(by:) 的方法莲祸,它會(huì)根據(jù)你所提供的用于排序的閉包函數(shù)將已知類型數(shù)組中的值進(jìn)行排序蹂安。一旦排序完成,sorted(by:) 方法會(huì)返回一個(gè)與原數(shù)組大小相同锐帜,包含同類型元素且元素已正確排序的新數(shù)組田盈。原數(shù)組不會(huì)被 sorted(by:) 方法修改。

下面的閉包表達(dá)式示例使用 sorted(by:) 方法對一個(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建炫。

該例子對一個(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 前衍慎。對于字符串中的字符來說转唉,“大于”表示“按照字母順序較晚出現(xiàn)”。這意味著字母 "B" 大于字母 "A" 西饵,字符串 "Tom" 大于字符串 "Tim"酝掩。該閉包將進(jìn)行字母逆序排序,"Barry" 將會(huì)排在 "Alex" 之前眷柔。

然而期虾,以這種方式來編寫一個(gè)實(shí)際上很簡單的表達(dá)式(a > b),確實(shí)太過繁瑣了驯嘱。對于這個(gè)例子來說镶苞,利用閉包表達(dá)式語法可以更好地構(gòu)造一個(gè)內(nèi)聯(lián)排序閉包


閉包表達(dá)式語法

閉包表達(dá)式語法有如下的一般形式:

 { (parameters) -> returnType in
     statements
 }

閉包表達(dá)式參數(shù) 可以是 in-out 參數(shù),但不能設(shè)定默認(rèn)值鞠评。也可以使用具名的可變參數(shù)(譯者注:但是如果可變參數(shù)不放在參數(shù)列表的最后一位的話茂蚓,調(diào)用閉包的時(shí)時(shí)編譯器將報(bào)錯(cuò)。可參考這里)聋涨。元組也可以作為參數(shù)和返回值晾浴。

下面的例子展示了之前 backward(::) 函數(shù)對應(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ù)和返回值類型都寫在大括號內(nèi)茂腥,而不是大括號外狸涌。

閉包的函數(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)用保持不變般渡,一對圓括號仍然包裹住了方法的整個(gè)參數(shù)懒豹。然而,參數(shù)現(xiàn)在變成了內(nèi)聯(lián)閉包诊杆。


根據(jù)上下文推斷類型

因?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ù)周圍的括號也可以被省略:

 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á)式閉包隱式返回

單行表達(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)鍵字可以省略砰碴。


參數(shù)名稱縮寫

Swift 自動(dòng)為內(nèi)聯(lián)閉包提供了參數(shù)名稱縮寫功能,你可以直接通過 0莱坎,1衣式,$2 來順序調(diào)用閉包的參數(shù),以此類推檐什。

如果你在閉包表達(dá)式中使用參數(shù)名稱縮寫,你可以在閉包定義中省略參數(shù)列表弱卡,并且對應(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ù)瓮具。


運(yùn)算符方法
實(shí)際上還有一種更簡短的方式來編寫上面例子中的閉包表達(dá)式。Swift 的 String 類型定義了關(guān)于大于號(>)的字符串實(shí)現(xiàn)凡人,其作為一個(gè)函數(shù)接受兩個(gè) String 類型的參數(shù)并返回 Bool 類型的值名党。而這正好與 sorted(by:) 方法的參數(shù)需要的函數(shù)類型相符合。因此挠轴,你可以簡單地傳遞一個(gè)大于號传睹,Swift 可以自動(dòng)推斷出你想使用大于號的字符串函數(shù)實(shí)現(xiàn):

 reversedNames = names.sorted(by: >)

更多關(guān)于運(yùn)算符方法的內(nèi)容請查看運(yùn)算符方法

尾隨閉包

如果你需要將一個(gè)很長的閉包表達(dá)式作為最后一個(gè)參數(shù)傳遞給函數(shù)岸晦,可以使用尾隨閉包來增強(qiáng)函數(shù)的可讀性欧啤。尾隨閉包是一個(gè)書寫在函數(shù)括號之后的閉包表達(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:) 方法參數(shù)的字符串排序閉包可以改寫為:

 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ù)組中的元素一一對應(yīng)的映射后的值俩垃。

下例介紹了如何在 map(_:) 方法中使用尾隨閉包將 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)建了一個(gè)整型數(shù)位和它們英文版本名字相映射的字典。同時(shí)還定義了一個(gè)準(zhǔn)備轉(zhuǎn)換為字符串?dāng)?shù)組的整型數(shù)組汰寓。

你現(xiàn)在可以通ccers 數(shù)組的 map(_:) 方法來創(chuàng)建對應(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)對其進(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è)嘆號(!),因?yàn)樽值湎聵?biāo)返回一個(gè)可選值(optional value)蟹演,表明該鍵不存在時(shí)會(huì)查找失敗风钻。在上例中,由于可以確定 number % 10 總是 digitNames 字典的有效下標(biā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(_:) 方法的括號內(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)容,請查看函數(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ì)改為捕獲并保存一份對值的拷貝于样。 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)引用蘸劈。更多信息昏苏,請參考閉包引起的循環(huán)強(qiáng)引用

閉包是引用類型

上面的例子中威沫,incrementBySeven 和 incrementByTen 都是常量贤惯,但是這些常量指向的閉包仍然可以增加其捕獲的變量的值。這是因?yàn)楹瘮?shù)和閉包都是引用類型棒掠。

無論你將函數(shù)或閉包賦值給一個(gè)常量還是變量孵构,你實(shí)際上都是將常量或變量的值設(shè)置為對應(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粹舵。相對的,傳遞到 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)閉包是一種自動(dòng)創(chuàng)建的閉包,用于包裝傳遞給函數(shù)作為參數(shù)的表達(dá)式稠茂。這種閉包不接受任何參數(shù),當(dāng)它被調(diào)用的時(shí)候情妖,會(huì)返回被包裝在其中的表達(dá)式的值睬关。這種便利語法讓你能夠省略閉包的花括號,用一個(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í)行居扒。延遲求值對于那些有副作用(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ì)被移除这揣。請注意悔常,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)閉包」奁欤現(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ù)作用域选调。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市仁堪,隨后出現(xiàn)的幾起案子哮洽,更是在濱河造成了極大的恐慌,老刑警劉巖弦聂,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鸟辅,死亡現(xiàn)場離奇詭異,居然都是意外死亡莺葫,警方通過查閱死者的電腦和手機(jī)匪凉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捺檬,“玉大人再层,你說我怎么就攤上這事”の常” “怎么了聂受?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長烤镐。 經(jīng)常有香客問我蛋济,道長,這世上最難降的妖魔是什么炮叶? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任碗旅,我火速辦了婚禮,結(jié)果婚禮上镜悉,老公的妹妹穿的比我還像新娘扛芽。我一直安慰自己,他們只是感情好积瞒,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著登下,像睡著了一般茫孔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上被芳,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天缰贝,我揣著相機(jī)與錄音,去河邊找鬼畔濒。 笑死剩晴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播赞弥,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼毅整,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绽左?” 一聲冷哼從身側(cè)響起悼嫉,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拼窥,沒想到半個(gè)月后戏蔑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鲁纠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年总棵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片改含。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡情龄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出候味,到底是詐尸還是另有隱情刃唤,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布白群,位于F島的核電站尚胞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏帜慢。R本人自食惡果不足惜笼裳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望粱玲。 院中可真熱鬧躬柬,春花似錦、人聲如沸抽减。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卵沉。三九已至颠锉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間史汗,已是汗流浹背琼掠。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留停撞,地道東北人瓷蛙。 一個(gè)月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓悼瓮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親艰猬。 傳聞我的和親對象是個(gè)殘疾皇子横堡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容