鍍金池/ 教程/ iOS/ 閉包(Closures)
特性(Attributes)
Access Control 權(quán)限控制的黑與白
基本運(yùn)算符(Basic Operators)
基礎(chǔ)部分(The Basics)
閉包(Closures)
擴(kuò)展
泛型參數(shù)(Generic Parameters and Arguments)
訪問控制和 protected
語句(Statements)
模式(Patterns)
WWDC 里面的那個(gè)“大炮打氣球”
關(guān)于語言參考(About the Language Reference)
語法總結(jié)(Summary of the Grammar)
嵌套類型
類型(Types)
Swift 初見(A Swift Tour)
泛型
枚舉(Enumerations)
高級(jí)運(yùn)算符
繼承
析構(gòu)過程
關(guān)于 Swift(About Swift)
訪問控制
類和結(jié)構(gòu)體
內(nèi)存安全
Swift 與 C 語言指針友好合作
協(xié)議
屬性(Properties)
可選類型完美解決占位問題
錯(cuò)誤處理
字符串和字符(Strings and Characters)
聲明(Declarations)
自動(dòng)引用計(jì)數(shù)
Swift 里的值類型與引用類型
表達(dá)式(Expressions)
Swift 文檔修訂歷史
造個(gè)類型不是夢(mèng)-白話 Swift 類型創(chuàng)建
歡迎使用 Swift
詞法結(jié)構(gòu)(Lexical Structure)
集合類型(Collection Types)
下標(biāo)
方法(Methods)
可選鏈?zhǔn)秸{(diào)用
版本兼容性
類型轉(zhuǎn)換
構(gòu)造過程
The Swift Programming Language 中文版
函數(shù)(Functions)
Swift 教程
控制流(Control Flow)

閉包(Closures)


1.0 翻譯:wh1100717 校對(duì):lyuka

2.0 翻譯+校對(duì):100mango

2.1 翻譯:100mango, magicdict 校對(duì):shanks

2.2 翻譯+校對(duì):SketchK 2016-05-12

3.0 翻譯:Lanford 2016-09-19

3.0.1,shanks,2016-11-12

4.0 校對(duì):kemchenj 2017-09-21

4.1 翻譯+校對(duì):mylittleswift

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

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

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

注意

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

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

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

Swift 的閉包表達(dá)式擁有簡(jiǎn)潔的風(fēng)格,并鼓勵(lì)在常見場(chǎng)景中進(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á)式是一種利用簡(jiǎn)潔語法構(gòu)建內(nèi)聯(lián)閉包的方式。閉包表達(dá)式提供了一些語法優(yōu)化,使得撰寫閉包變得簡(jiǎn)單明了。下面閉包表達(dá)式的例子通過使用幾次迭代展示了 sorted(by:) 方法定義和語法優(yōu)化的方式。每一次迭代都用更簡(jiǎn)潔的方式描述了相同的功能。

排序方法

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:) 方法對(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í)際上很簡(jiǎn)單的表達(dá)式(a > b),確實(shí)太過繁瑣了。對(duì)于這個(gè)例子來說,利用閉包表達(dá)式語法可以更好地構(gòu)造一個(gè)內(nèi)聯(lián)排序閉包。

閉包表達(dá)式語法

閉包表達(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)閉包。

根據(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ù)周圍的括號(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ù)組的排序,因此讀者能夠推測(cè)出這個(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ù)列表,并且對(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ù)。

運(yùn)算符方法

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

reversedNames = names.sorted(by: >)

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

尾隨閉包

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

reversedNames = names.sorted() { $0 > $1 }

如果閉包表達(dá)式是函數(shù)或方法的唯一參數(shù),則當(dāng)你使用尾隨閉包時(shí),你甚至可以把 () 省略掉:

reversedNames = names.sorted { $0 > $1 }

當(dāng)閉包非常長(zhǎ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 中,如果 number16,則返回 658 返回 8,510 返回 0。)

number 變量之后除以 10。因?yàn)槠涫钦麛?shù),在計(jì)算過程中未除盡部分被忽略。因此 16 變成了 1,58 變成了 5,510 變成了 51。

整個(gè)過程重復(fù)進(jìn)行,直到 number /= 100,這時(shí)閉包會(huì)將字符串 output 返回,而 map(_:) 方法則會(huì)將字符串添加到映射數(shù)組中。

在上面的例子中,通過尾隨閉包語法,優(yōu)雅地在函數(shù)后封裝了閉包的具體功能,而不再需要將整個(gè)閉包包裹在 map(_:) 方法的括號(hào)內(nèi)。

值捕獲

閉包可以在其被定義的上下文中捕獲常量或變量。即使定義這些常量和變量的原作用域已經(jīng)不存在,閉包仍然可以在閉包函數(shù)體內(nèi)引用和修改這些值。

Swift 中,可以捕獲值的閉包的最簡(jiǎn)單形式是嵌套函數(shù),也就是定義在其他函數(shù)的函數(shù)體內(nèi)的函數(shù)。嵌套函數(shù)可以捕獲其外部函數(shù)所有的參數(shù)以及定義的常量和變量。

舉個(gè)例子,這有一個(gè)叫做 makeIncrementer 的函數(shù),其包含了一個(gè)叫做 incrementer 的嵌套函數(shù)。嵌套函數(shù) incrementer() 從上下文中捕獲了兩個(gè)值,runningTotalamount。捕獲這些值之后,makeIncrementerincrementer 作為閉包返回。每次調(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è)簡(jiǎn)單類型的值。該函數(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ù)簡(jiǎn)單地使 runningTotal 增加 amount,并將其返回。

如果我們單獨(dú)考慮嵌套函數(shù) incrementer(),會(huì)發(fā)現(xiàn)它有些不同尋常:

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

incrementer() 函數(shù)并沒有任何參數(shù),但是在函數(shù)體內(nèi)訪問了 runningTotalamount 變量。這是因?yàn)樗鼜耐鈬瘮?shù)捕獲了 runningTotalamount 變量的引用。捕獲引用保證了 runningTotalamount 變量在調(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 變量增加 10incrementer 函數(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)引用。

閉包是引用類型

上面的例子中,incrementBySevenincrementByTen 都是常量,但是這些常量指向的閉包仍然可以增加其捕獲的變量的值。這是因?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)閉包是一種自動(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ù)作用域。