Swift 閉包

Swift 中的閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。類似于OC中的Block以及其他函數(shù)的匿名函數(shù)。閉包可以捕獲和存儲(chǔ)其所在上下文中任意常量和變量的引用,被稱為包裹常量和變量。Swift可以為你管理在捕獲過程中涉及到的所有內(nèi)存操作

函數(shù)與閉包的關(guān)系

全局和嵌套函數(shù)實(shí)際上也是特殊的閉包,閉包有如下三種形式:
1.全局函數(shù)是一個(gè)有名字但不會(huì)捕獲任何值的閉包
2.嵌套函數(shù)是一個(gè)有名字並可以捕獲其封閉函數(shù)域內(nèi)值的閉包
3.閉包表達(dá)式是一個(gè)利用輕量級(jí)語(yǔ)法所寫的可以捕獲其上下文中變量或常量值的匿名閉包

  • 閉包表達(dá)式
  • 尾隨閉包
  • 值捕獲
  • 閉包是引用類型
  • 逃逸閉包
  • 自動(dòng)閉包

閉包表達(dá)式一般形式:

{ (parameters) -> returnType in
    statements
}

下面以Swift標(biāo)準(zhǔn)庫(kù)中的sorted(by:)方法為例,展示閉包的各種書寫格式。它會(huì)根據(jù)閉包函數(shù)中的實(shí)現(xiàn)將數(shù)組中的值進(jìn)行排序,返回一個(gè)與原數(shù)組大小相同,包含相同元素且重新排序的新數(shù)組。原數(shù)組不會(huì)被 sorted(by:)方法修改

最復(fù)雜的形式

原數(shù)組: let names = ["Linda", "Cindy", "Benty", "Dandy"]
排序后數(shù)組:sortNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 < s2 } ) print(sortNames) // [ "Benty", "Cindy", "Dandy","Linda"]
如果第一個(gè)字符串s1小于 第二個(gè)字符串s2, 函數(shù)會(huì)返回 true,在新的數(shù)組中 s1 應(yīng)該出現(xiàn)在 s2 前。對(duì)于字符串中的字符來說,“小于”表示“按照字母順序較晚出現(xiàn)”。這意味著字母 "A" 小于字母 "B" ,字符串 "Ab" 小于字符串 "Ac"。該閉包將進(jìn)行字母順序排序,"Benty" 將會(huì)排在 "Cindy" 之前。

上下文推斷類型

排序閉包函數(shù)作為sorted(by:)方法的參數(shù)傳入,Swift 可以推斷其參數(shù)和返回值的類型。sorted(by:)方法被一個(gè)字符串?dāng)?shù)組調(diào)用,因此其參數(shù)必須是 (String, String) -> Bool 類型。這意味著 (String, String) 和 Bool 類型并不需要作為閉包表達(dá)式定義的一部分。因?yàn)樗械念愋投伎梢员徽_推斷,返回箭頭(->)和圍繞在參數(shù)周圍的括號(hào)也可以被省略:
sortNames = names.sorted(by: { s1, s2 in return s1 < s2 } )
盡管如此,你仍然可以明確寫出有著完整格式的閉包。如果完整格式的閉包能夠提高代碼的可讀性,則我們更鼓勵(lì)采用完整格式的閉包。而在 sorted(by:) 方法這個(gè)例子里,顯然閉包的目的就是排序。由于這個(gè)閉包是為了處理字符串?dāng)?shù)組的排序,因此讀者能夠推測(cè)出這個(gè)閉包是用于字符串處理的。

單表達(dá)式閉包隱式返回

sortNames = 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)成:
sortNames = 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):
sortNames = names.sorted(by: <)

尾隨閉包

如果你需要將一個(gè)很長(zhǎng)的閉包表達(dá)式作為最后一個(gè)參數(shù)傳遞給函數(shù),可以使用尾隨閉包來增強(qiáng)函數(shù)的可讀性。尾隨閉包是一個(gè)書寫在函數(shù)括號(hào)之后的閉包表達(dá)式,函數(shù)支持將其作為最后一個(gè)參數(shù)調(diào)用。在使用尾隨閉包時(shí),你不用寫出它的參數(shù)標(biāo)簽。
sortNames = names.sorted() { $0 < $1 }
如果閉包表達(dá)式是函數(shù)或方法的唯一參數(shù),則當(dāng)你使用尾隨閉包時(shí),你甚至可以把 () 省略掉:sortNames =names.sorted { $0 < $1 }

值捕獲

閉包可以在其被定義的上下文中捕獲常量或變量。即使定義這些常量和變量的原作用域已經(jīng)不存在,閉包仍然可以在閉包函數(shù)體內(nèi)引用和修改這些值。
舉個(gè)例子,這有一個(gè)叫做 makeIncrementor 的函數(shù),其包含了一個(gè)叫做 incrementor 的嵌套函數(shù)。嵌套函數(shù) incrementor() 從上下文中捕獲了兩個(gè)值,runningTotalamount。捕獲這些值之后,makeIncrementorincrementor 作為閉包返回。每次調(diào)用 incrementor 時(shí),其會(huì)以 amount 作為增量增加 runningTotal 的值。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incremented
}

makeIncrementor 返回類型為 () -> Int。這意味著其返回的是一個(gè)函數(shù),而非一個(gè)簡(jiǎn)單類型的值。該函數(shù)在每次調(diào)用時(shí)不接受參數(shù),只返回一個(gè) Int 類型的值 makeIncrementer(forIncrement:) 函數(shù)定義了一個(gè)初始值為 0 的整型變量 runningTotal,用來存儲(chǔ)當(dāng)前總計(jì)數(shù)值。該值為 incrementor 的返回值
makeIncrementer(forIncrement:) 有一個(gè) Int 類型的參數(shù),其外部參數(shù)名為 forIncrement,內(nèi)部參數(shù)名為 amount,該參數(shù)表示每次 incrementor 被調(diào)用時(shí) runningTotal 將要增加的量。makeIncrementer 函數(shù)還定義了一個(gè)嵌套函數(shù) incrementor,用來執(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 依舊存在。

閉包是引用類型

上面的例子中,incrementBySeven 和 incrementByTen 都是常量,但是這些常量指向的閉包仍然可以增加其捕獲的變量的值。這是因?yàn)楹瘮?shù)和閉包都是引用類型。無(wú)論你將函數(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() 
} 
        var x = 10
        func doSomething() { 
        someFunctionWithEscapingClosure { 
           //逃逸閉包 需要顯式地引用 self
          //調(diào)用doSomething 不會(huì)執(zhí)行閉包
            self.x = 100 
       } 
       someFunctionWithNonescapingClosure { 
          //因?yàn)椴皇翘右蓍]包,調(diào)用doSomething 執(zhí)行閉包
           x = 200
      } 
   } 

      doSomething() 
      print(x) // 打印出 "200" 
      completionHandlers.first?()
      print(x)  // 打印出 "100"  逃逸閉包調(diào)用

自動(dòng)閉包

自動(dòng)閉包是一種自動(dòng)創(chuàng)建的閉包,用于包裝傳遞給函數(shù)作為參數(shù)的表達(dá)式。這種閉包不接受任何參數(shù),當(dāng)它被調(diào)用的時(shí)候,會(huì)返回被包裝在其中的表達(dá)式的值。這種便利語(yǔ)法讓你能夠省略閉包的花括號(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í)求值行為。

   let customersInLine = ["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 特性。

    let customersInLine = ["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ù)作用域。

結(jié)束

本文內(nèi)容摘自中文版 Apple 官方 Swift 教程,對(duì)于初學(xué)swift的朋友有較大幫助,下面是蘋果官方文檔鏈接和github中文文檔鏈接 官方文檔 中文文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 閉包可以從定義它們的上下文中捕獲和存儲(chǔ)對(duì)任何常量和變量的引用。 這被稱為關(guān)閉這些常量和變量。 Swift處理所有的...
    Joker_King閱讀 642評(píng)論 0 2
  • 閉包是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代...
    窮人家的孩紙閱讀 1,813評(píng)論 1 5
  • Swift-閉包 Swift閉包的含義 閉包是自包含的功能代碼塊,可以用作函數(shù)的參數(shù)或者返回值 閉包可以捕獲上下文...
    stackJolin閱讀 1,302評(píng)論 0 2
  • * 閉包 是自包含的函數(shù)代碼塊,可以在代碼中被傳遞和使用。swift中的閉包和Objective-C中的代碼塊(b...
    EndEvent閱讀 894評(píng)論 4 8
  • 本章將會(huì)介紹 閉包表達(dá)式尾隨閉包值捕獲閉包是引用類型逃逸閉包自動(dòng)閉包枚舉語(yǔ)法使用Switch語(yǔ)句匹配枚舉值關(guān)聯(lián)值原...
    寒橋閱讀 1,631評(píng)論 0 3

友情鏈接更多精彩內(nèi)容