【Swift 3.1】07 - 閉包 (Closures)

閉包 (Closures)

自從蘋果2014年發(fā)布Swift,到現(xiàn)在已經(jīng)兩年多了,而Swift也來到了3.1版本。去年利用工作之余,共花了兩個多月的時間把官方的Swift編程指南看完。現(xiàn)在整理一下筆記,回顧一下以前的知識。有需要的同學可以去看官方文檔>>


閉包是具有一定功能的代碼塊。Swift的閉包類似于C和OC中的block和其他編程語言的匿名函數(shù)。

全局和嵌套方法實際上都是屬于閉包的特殊情況。閉包有以下三種形式:

  • 全局方法:有一個名字,不會不會捕獲任何值
  • 嵌套方法:有一個名字,可以從包含這個嵌套方法的方法內部捕獲值
  • 閉包語句:沒有名字,可以從包含它的上下文捕獲值

Swift的閉包語句經(jīng)過了一系列的優(yōu)化變得非常簡單、整潔和清晰:

  • 根據(jù)上下文推斷參數(shù)和返回值的類型
  • 一個閉包有隱藏的返回值
  • 簡略的參數(shù)名
  • 后置閉包語法

閉包表達式 (Closure Expressions)

排序方法 (Sorted Methods)

Swift標準庫中有一個方法sorted(by:),可以用來對一個數(shù)組的值排序。當排序執(zhí)行完成時,返回一個排好序的數(shù)組,并且不會修改原數(shù)組。sorted(by:)方法接收一個具有兩個與數(shù)組元素類型相同的參數(shù)、并返回一個布爾值的閉包,閉包的返回值說明了是正序還是倒序。

例如下面這個例子,對一個[String]類型的數(shù)組,排序閉包需要一個(String, String) -> Bool的方法:

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reverseNaems = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

然而,上面這種寫法不夠簡潔,我們可以使用閉包表達式語法來寫。

閉包表達式語法 (Closure Expression Syntax)

閉包表達式語法的通用形式如下:

{ (parameters) -> return type in
    statements
}

閉包表達式語法的參數(shù)可以是in-out參數(shù),但是不能有默認值,可以使用可變參數(shù),多元組也可以作為參數(shù)和返回值。

backward(_:_:)的閉包表達式語法:

reverseNames = names.sorted(by: { (s1: String, s2: String) -> Bool in 
    return s1 > s2
})
根據(jù)上下文推斷類型 (Inferring Type From Context)

因為分類閉包當做參數(shù)被傳入一個方法,Swift能推斷參數(shù)的類型和返回值的類型。因為參數(shù)和返回值的類型都能被推斷出來,所以參數(shù)的類型和返回值類型都可以忽略,寫成:

reversNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
隱藏Return的單個表達式閉包 (Implicit Returns from Single-Expression Closures)

單個表達式閉包可以通過刪除return關鍵字來隱式地返回單個表達式的結果:

reveresNames = names.sorted(by: s1, s2 in s1 > s2)
簡略參數(shù)名 (Shorthand Argument Names)

Swift可以自動提供簡略參數(shù)名給單行閉包,這些簡略參數(shù)名是被用來引用于閉包的參數(shù),例如$0$1$2等等。

如果在閉包中使用這種形式,可以在定義包時把參數(shù)省略,in關鍵字也可以省略:

reverseNames = names.sorted(by: { $0 > $1 })

$0$1是閉包的第一和第二個參數(shù)。

運算符方法 (Operator Methods)

其實上面的閉包還可以用更簡短的方式來實現(xiàn)。Swift的String類型把>定義為一個具有兩個String類型并返回布爾值得方法。這剛好符合sorted(by:)方法需要的閉包參數(shù)。所以上面的例子可以簡寫成:

reverseNames = names.sorted(by: >)

后置閉包 (Trailing Closures)

如果我們需要傳入一個很長的閉包作為參數(shù),并且這個參數(shù)是最后一個參數(shù),后置閉包是非常有用的。當使用后置閉包語法時,不需要寫參數(shù)的標簽:

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}
 
// 調用時沒有使用后置閉包
 
someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})
 
// 調用時使用后置閉包
 
someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

上面提到的把名字倒序排列的例子中,可以使用后置閉包語法寫成:

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

如果方法的參數(shù)中只有一個方法類型的參數(shù),在使用后置閉包語法時,可以把()省略:

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

當閉包非常長而且不能用一行代碼寫完時,后置閉包是非常有用的。例如,Swift的Array類型有一個map(_:)方法,這個方法需要一個閉包表達式作為它唯一的參數(shù)。這個閉包會被數(shù)組中的每個元素調用一次,并返回一個與元素相關的值。map(_:)方法執(zhí)行完后,會返回一個新的數(shù)組,數(shù)組包含著所有與各個元素對應的值,并且順序與原素組相同。

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]

let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitName[number % 10]! + output
        number /= 10
    } while number > 0
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

map(_:)方法會讓數(shù)組的每一個元素調用一次閉包。在閉包中,我們無需指定參數(shù)number的類型,因為number的類型可以從數(shù)組的元素類型中推斷出來。

number變量使用閉包的number參數(shù)參數(shù)來初始化,以保證閉包后面的代碼中能修改number的值,但是閉包參數(shù)number還是屬于常量。

捕獲值 (Capturing Values)

閉包可以從包含這個閉包的方法中捕獲這個方法內部定義的常量或變量,然后閉包可以修改這些常量或者變量,即使這些常量和變量不再存在。

在Swift中,最簡單的能捕獲值的閉包形式就是嵌套方法。一個嵌套方法可以捕獲包含嵌套方法的方法參數(shù)和內部定義的常量或者變量。

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

incrementer()方法沒有參數(shù),從包含它的方法中引用runningTotalamount,這種引用是通過捕獲對runningTotalamount的指針實現(xiàn)的。通過捕獲指針保證了runningTotalamountmakeIncrementer執(zhí)行完之后不會消失,并且在下一次調用makeIncrementer時還可以使用。

注意:因為優(yōu)化,如果一個值在閉包中沒有被修改,Swift會捕獲和存儲這個值的副本。Swift還會處理內存管理問題,當這些變量不再使用時,Swift會把他們銷毀。

下面是使用makeIncrementer的一個例子:

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

如果創(chuàng)建第二個incrementer,它會有自己的一個對runningTotal的引用:

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

然后再調用之前的incrementByTenrunningTotal的值會繼續(xù)往上增加,并且不會對incrementBySeven引用的runningTotal造成影響。

注意:如果把一個閉包賦值給一個類對實例的屬性,這個閉包又通過引用這個類對象的實例或者成員來捕獲值,這將會造成在閉包和類實例之間的循環(huán)引用。

閉包是引用類型 (Closures Are Reference Types)

在上面的例子中,incrementBySeveincrementByTen都是常量,但是這兩個常量引用的閉包還可以讓runningTotal繼續(xù)增加。這是因為方法和閉包都是引用類型。

不管在什么時候,把方法和閉包賦值給變量和常量,實際上常量或者變量指向了方法和閉包。那么這就意味著如果把一個閉包賦值給不同的變量或者常量,這些常量和變量實際上是引用這同一個閉包:

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen
// returns a value of 50

逃逸閉包 (Escaping Closures)

當一個閉包作為參數(shù)傳給一個方法時,但是在方法返回之后才調用,那么這個閉包被稱為逃逸閉包。在聲明方法時,在方法參數(shù)類型之前使用@escaping來說明這個閉包允許“逃脫”。

一個閉包能“逃脫”的一種方式,就是把這個閉包存儲在方法之外定義的變量中。例如,很多方法開啟一個異步操作,并用一個閉包參數(shù)作為一個completion handler。在異步操作開始之后,馬上返回。但是閉包在異步操作完成之前并不會調用,閉包需要“逃脫”,在后面調用:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

在這個方法中,如果不加上@escaping,將會編譯錯誤。

使用@escaping標記一個閉包,意味著在閉包中需要明確的引用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)
// Prints "200"
 
completionHandlers.first?()
print(instance.x)
// Prints "100"

自動閉包 (Autoclosures)

一個閉包自動創(chuàng)建并被包裝成一個表達式,然后作為參數(shù)傳入方法,這個閉包就被稱為自動閉包。

一個自動閉包可以延遲執(zhí)行,因為閉包中的代碼在被調用之前并不會執(zhí)行。延遲執(zhí)行在一些有副作用或者計算昂貴的代碼中非常有用,因為我們可以控制代碼何時執(zhí)行:

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
 
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

即使customersInLine的第一個元素在閉包中被刪除了,但是這個元素在閉包執(zhí)行之前并不會被刪除。注意: customerProvider不是String類型,而是() -> String

把一個閉包作為參數(shù)傳給方法也是一樣:

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

使用@autoclosure

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

注意:過度使用自動閉包會降低代碼的可讀性。

如果一個自動閉包想要“逃脫”,同時使用@autoclosure@escaping

// customersInLine is ["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.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

第七部分完。下個部分:【Swift 3.1】08 - 枚舉 (Enumerations)


如果有錯誤的地方,歡迎指正!謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容