Swift學習筆記----閉包(Closures)

閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代碼塊(blocks)以及其他一些編程語言中的匿名函數比較相似。
閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用。被稱為包裹常量和變量。 Swift 會為你管理在捕獲過程中涉及到的所有內存操作。
注意 如果你不熟悉捕獲(capturing)這個概念也不用擔心,你可以在值捕獲章節對其進行詳細了解。

函數章節中介紹的全局和嵌套函數實際上也是特殊的閉包,閉包采取如下三種形式之一:
全局函數是一個有名字但不會捕獲任何值的閉包
嵌套函數是一個有名字并可以捕獲其封閉函數域內值的閉包
閉包表達式是一個利用輕量級語法所寫的可以捕獲其上下文中變量或常量值的匿名閉包

Swift 的閉包表達式擁有簡潔的風格,并鼓勵在常見場景中進行語法優化,主要優化如下:
利用上下文推斷參數和返回值類型
隱式返回單表達式閉包,即單表達式閉包可以省略 return
關鍵字
參數名稱縮寫
尾隨閉包語法

閉包表達式語法

閉包表達式語法有如下的一般形式:

{ 
(parameters) -> returnType in statements
}```

*閉包表達式參數* 可以是 in-out 參數,但不能設定默認值。也可以使用具名的可變參數(譯者注:但是如果可變參數不放在參數列表的最后一位的話,調用閉包的時時編譯器將報錯。可參考[這里](http://stackoverflow.com/questions/39548852/swift-3-0-closure-expression-what-if-the-variadic-parameters-not-at-the-last-pl))。元組也可以作為參數和返回值。
下面的例子展示了之前`` backward(_:_:)`` 函數對應的閉包表達式版本的代碼:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})```

需要注意的是內聯閉包參數和返回值類型聲明與 backward(_:_:)函數類型聲明相同。在這兩種方式中,都寫成了(s1: String, s2: String) -> Bool。然而在內聯閉包表達式中,函數和返回值類型都寫在大括號內,而不是大括號外。

閉包的函數體部分由關鍵字in引入。該關鍵字表示閉包的參數和返回值類型定義已經完成,閉包函數體即將開始。

由于這個閉包的函數體部分如此短,以至于可以將其改寫成一行代碼:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )```
該例中 sorted(by:) 方法的整體調用保持不變,一對圓括號仍然包裹住了方法的整個參數。然而,參數現在變成了內聯閉包。

##根據上下文推斷類型

因為排序閉包函數是作為 sorted(by:) 方法的參數傳入的,Swift 可以推斷其參數和返回值的類型。sorted(by:) 方法被一個字符串數組調用,因此其參數必須是 (String, String) -> Bool 類型的函數。這意味著 (String, String) 和 Bool 類型并不需要作為閉包表達式定義的一部分。因為所有的類型都可以被正確推斷,返回箭頭(->)和圍繞在參數周圍的括號也可以被省略:

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )```

實際上,通過內聯閉包表達式構造的閉包作為參數傳遞給函數或方法時,總是能夠推斷出閉包的參數和返回值類型。這意味著閉包作為函數或者方法的參數時,你幾乎不需要利用完整格式構造內聯閉包。

盡管如此,你仍然可以明確寫出有著完整格式的閉包。如果完整格式的閉包能夠提高代碼的可讀性,則我們更鼓勵采用完整格式的閉包。而在 sorted(by:) 方法這個例子里,顯然閉包的目的就是排序。由于這個閉包是為了處理字符串數組的排序,因此讀者能夠推測出這個閉包是用于字符串處理的。

單表達式閉包隱式返回

單行表達式閉包可以通過省略 return 關鍵字來隱式返回單行表達式的結果,如上版本的例子可以改寫為:

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )```
在這個例子中,sorted(by:) 方法的參數類型明確了閉包必須返回一個 Bool 類型值。因為閉包函數體只包含了一個單一表達式(s1 > s2),該表達式返回 Bool 類型值,因此這里沒有歧義,return 關鍵字可以省略。
##參數名稱縮寫

Swift 自動為內聯閉包提供了參數名稱縮寫功能,你可以直接通過 $0,$1,$2 來順序調用閉包的參數,以此類推。

如果你在閉包表達式中使用參數名稱縮寫,你可以在閉包定義中省略參數列表,并且對應參數名稱縮寫的類型會通過函數類型進行推斷。in關鍵字也同樣可以被省略,因為此時閉包表達式完全由閉包函數體構成:

reversedNames = names.sorted(by: { $0 > $1 } )```
在這個例子中,$0和$1表示閉包中第一個和第二個 String 類型的參數。

運算符方法

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

reversedNames = names.sorted(by: >)```

##尾隨閉包

如果你需要將一個很長的閉包表達式作為最后一個參數傳遞給函數,可以使用尾隨閉包來增強函數的可讀性。尾隨閉包是一個書寫在函數括號之后的閉包表達式,函數支持將其作為最后一個參數調用。在使用尾隨閉包時,你不用寫出它的參數標簽:

func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函數體部分
}

// 以下是不使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure(closure: {
// 閉包主體部分
})

// 以下是使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure() {
// 閉包主體部分
}```

閉包表達式語法一節中作為 sorted(by:)
方法參數的字符串排序閉包可以改寫為:

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

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

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

當閉包非常長以至于不能在一行中進行書寫時,尾隨閉包變得非常有用。舉例來說,Swift 的 Array 類型有一個 map(_:) 方法,這個方法獲取一個閉包表達式作為其唯一參數。該閉包函數會為數組中的每一個元素調用一次,并返回該元素所映射的值。具體的映射方式和返回值類型由閉包來指定。

當提供給數組的閉包應用于每個數組元素后,map(_:) 方法將返回一個新的數組,數組中包含了與原數組中的元素一一對應的映射后的值。

下例介紹了如何在 map(_:) 方法中使用尾隨閉包將 Int 類型數組 [16, 58, 510] 轉換為包含對應 String 類型的值的數組["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]

如上代碼創建了一個整型數位和它們英文版本名字相映射的字典。同時還定義了一個準備轉換為字符串數組的整型數組。

你現在可以通過傳遞一個尾隨閉包給 numbers 數組的 map(_:) 方法來創建對應的字符串版本數組:

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 常量被推斷為字符串類型數組,即 [String]
// 其值為 ["OneSix", "FiveEight", "FiveOneZero"]```

map(_:) 為數組中每一個元素調用了一次閉包表達式。你不需要指定閉包的輸入參數 number 的類型,因為可以通過要映射的數組類型進行推斷。

在該例中,局部變量 number 的值由閉包中的 number 參數獲得,因此可以在閉包函數體內對其進行修改,(閉包或者函數的參數總是常量),閉包表達式指定了返回類型為 String,以表明存儲映射值的新數組類型為 String。

閉包表達式在每次被調用的時候創建了一個叫做 output 的字符串并返回。其使用求余運算符(number % 10)計算最后一位數字并利用 digitNames 字典獲取所映射的字符串。這個閉包能夠用于創建任意正整數的字符串表示。

map(_:) 為數組中每一個元素調用了一次閉包表達式。你不需要指定閉包的輸入參數 number 的類型,因為可以通過要映射的數組類型進行推斷。

在該例中,局部變量 number 的值由閉包中的 number 參數獲得,因此可以在閉包函數體內對其進行修改,(閉包或者函數的參數總是常量),閉包表達式指定了返回類型為 String,以表明存儲映射值的新數組類型為 String。

>注意:
字典 digitNames 下標后跟著一個嘆號(!),因為字典下標返回一個可選值(optional value),表明該鍵不存在時會查找失敗。在上例中,由于可以確定 number % 10 總是 digitNames 字典的有效下標,因此嘆號可以用于強制解包 (force-unwrap) 存儲在下標的可選類型的返回值中的String類型的值。

從 digitNames 字典中獲取的字符串被添加到 output 的前部,逆序建立了一個字符串版本的數字。(在表達式 number % 10 中,如果 number 為 16,則返回 6,58 返回 8,510 返回 0。)

number 變量之后除以 10。因為其是整數,在計算過程中未除盡部分被忽略。因此 16 變成了 1,58 變成了 5,510 變成了 51。

整個過程重復進行,直到 number /= 10 為 0,這時閉包會將字符串 output 返回,而 ``map(_:) ``方法則會將字符串添加到映射數組中。

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

##值捕獲

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

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

舉個例子,這有一個叫做 makeIncrementer 的函數,其包含了一個叫做 incrementer 的嵌套函數。嵌套函數 incrementer() 從上下文中捕獲了兩個值,runningTotal 和 amount。捕獲這些值之后,makeIncrementer 將 incrementer 作為閉包返回。每次調用 incrementer 時,其會以 amount 作為增量增加 runningTotal 的值。

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

makeIncrementer 返回類型為 () -> Int。這意味著其返回的是一個函數,而非一個簡單類型的值。該函數在每次調用時不接受參數,只返回一個 Int 類型的值。

makeIncrementer(forIncrement:) 函數定義了一個初始值為 0 的整型變量 runningTotal,用來存儲當前總計數值。該值為 incrementer 的返回值。

makeIncrementer(forIncrement:) 有一個 Int 類型的參數,其外部參數名為 forIncrement,內部參數名為 amount,該參數表示每次 incrementer 被調用時 runningTotal 將要增加的量。makeIncrementer 函數還定義了一個嵌套函數 incrementer,用來執行實際的增加操作。該函數簡單地使 runningTotal 增加 amount,并將其返回。

如果我們單獨考慮嵌套函數 incrementer(),會發現它有些不同尋常:

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

incrementer() 函數并沒有任何參數,但是在函數體內訪問了 runningTotal 和 amount 變量。這是因為它從外圍函數捕獲了 runningTotal 和 amount 變量的引用。捕獲引用保證了 runningTotal 和 amount 變量在調用完 makeIncrementer 后不會消失,并且保證了在下一次執行 incrementer 函數時,runningTotal 依舊存在。

>注意 為了優化,如果一個值不會被閉包改變,或者在閉包創建后不會改變,Swift 可能會改為捕獲并保存一份對值的拷貝。 Swift 也會負責被捕獲變量的所有內存管理工作,包括釋放不再需要的變量。

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

let incrementByTen = makeIncrementer(forIncrement: 10)

該例子定義了一個叫做 incrementByTen 的常量,該常量指向一個每次調用會將其 runningTotal 變量增加 10 的 incrementer 函數。調用這個函數多次可以得到以下結果:

incrementByTen()
// 返回的值為10
incrementByTen()
// 返回的值為20
incrementByTen()
// 返回的值為30

如果你創建了另一個 incrementer,它會有屬于自己的引用,指向一個全新、獨立的 runningTotal 變量:

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回的值為7

再次調用原來的 incrementByTen 會繼續增加它自己的 runningTotal 變量,該變量和 incrementBySeven 中捕獲的變量沒有任何聯系:

incrementByTen()
// 返回的值為40```

注意:如果你將閉包賦值給一個類實例的屬性,并且該閉包通過訪問該實例或其成員而捕獲了該實例,你將在閉包和該實例間創建一個循環強引用。Swift 使用捕獲列表來打破這種循環強引用。更多信息,請參考閉包引起的循環強引用

閉包是引用類型

上面的例子中,incrementBySeven 和 incrementByTen 都是常量,但是這些常量指向的閉包仍然可以增加其捕獲的變量的值。這是因為函數和閉包都是引用類型。

無論你將函數或閉包賦值給一個常量還是變量,你實際上都是將常量或變量的值設置為對應函數或閉包的引用。上面的例子中,指向閉包的引用 incrementByTen 是一個常量,而并非閉包內容本身。

這也意味著如果你將閉包賦值給了兩個不同的常量或變量,兩個值都會指向同一個閉包:

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值為50```

##逃逸閉包

當一個閉包作為參數傳到一個函數中,但是這個閉包在函數返回之后才被執行,我們稱該閉包從函數中逃逸。當你定義接受閉包作為參數的函數時,你可以在參數名之前標注 @escaping,用來指明這個閉包是允許“逃逸”出這個函數的。

一種能使閉包“逃逸”出函數的方法是,將這個閉包保存在一個函數外部定義的變量中。舉個例子,很多啟動異步操作的函數接受一個閉包參數作為 completion handler。這類函數會在異步操作開始之后立刻返回,但是閉包直到異步操作結束后才會被調用。在這種情況下,閉包需要“逃逸”出函數,因為閉包需要在函數返回之后被調用。例如:

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

someFunctionWithEscapingClosure(_:) 函數接受一個閉包作為參數,該閉包被添加到一個函數外定義的數組中。如果你不將這個參數標記為 @escaping,就會得到一個編譯錯誤。

將一個閉包標記為 @escaping 意味著你必須在閉包中顯式地引用 self。比如說,在下面的代碼中,傳遞到 someFunctionWithEscapingClosure(:) 中的閉包是一個逃逸閉包,這意味著它需要顯式地引用 self。相對的,傳遞到 someFunctionWithNonescapingClosure(:) 中的閉包是一個非逃逸閉包,這意味著它可以隱式引用 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"

自動閉包

自動閉包是一種自動創建的閉包,用于包裝傳遞給函數作為參數的表達式。這種閉包不接受任何參數,當它被調用的時候,會返回被包裝在其中的表達式的值。這種便利語法讓你能夠省略閉包的花括號,用一個普通的表達式來代替顯式的閉包。

我們經常會調用采用自動閉包的函數,但是很少去實現這樣的函數。舉個例子來說,assert(condition:message:file:line:) 函數接受自動閉包作為它的 condition 參數和 message 參數;它的 condition 參數僅會在 debug 模式下被求值,它的 message 參數僅當 condition 參數為 false 時被計算求值。

自動閉包讓你能夠延遲求值,因為直到你調用這個閉包,代碼段才會被執行。延遲求值對于那些有副作用(Side Effect)和高計算成本的代碼來說是很有益處的,因為它使得你能控制代碼的執行時機。下面的代碼展示了閉包如何延時求值。

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 的第一個元素被移除了,不過在閉包被調用之前,這個元素是不會被移除的。如果這個閉包永遠不被調用,那么在閉包里面的表達式將永遠不會執行,那意味著列表中的元素永遠不會被移除。請注意,customerProvider 的類型不是 String,而是 () -> String,一個沒有參數且返回值為 String 的函數。

將閉包作為參數傳遞給函數時,你能獲得同樣的延時求值行為。

// 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:) 函數接受一個返回顧客名字的顯式的閉包。下面這個版本的 serve(customer:) 完成了相同的操作,不過它并沒有接受一個顯式的閉包,而是通過將參數標記為 @autoclosure 來接收一個自動閉包。現在你可以將該函數當作接受 String 類型參數(而非閉包)的函數來調用。customerProvider 參數將自動轉化為一個閉包,因為該參數被標記了 @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 會讓你的代碼變得難以理解。上下文和函數名應該能夠清晰地表明求值是被延遲執行的。

如果你想讓一個自動閉包可以“逃逸”,則應該同時使用 @autoclosure 和 @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(_:) 函數并沒有調用傳入的 customerProvider 閉包,而是將閉包追加到了 customerProviders 數組中。這個數組定義在函數作用域范圍外,這意味著數組內的閉包能夠在函數返回之后被調用。因此,customerProvider 參數必須允許“逃逸”出函數作用域。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容