Swift 中的閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。類似于OC中的Block以及其他函數的匿名函數。閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用,被稱為包裹常量和變量。Swift可以為你管理在捕獲過程中涉及到的所有內存操作
函數與閉包的關系
全局和嵌套函數實際上也是特殊的閉包,閉包有如下三種形式:
1.全局函數是一個有名字但不會捕獲任何值的閉包
2.嵌套函數是一個有名字並可以捕獲其封閉函數域內值的閉包
3.閉包表達式是一個利用輕量級語法所寫的可以捕獲其上下文中變量或常量值的匿名閉包
- 閉包表達式
- 尾隨閉包
- 值捕獲
- 閉包是引用類型
- 逃逸閉包
- 自動閉包
閉包表達式一般形式:
{ (parameters) -> returnType in
statements
}
下面以Swift
標準庫中的sorted(by:)
方法為例,展示閉包的各種書寫格式。它會根據閉包函數中的實現將數組中的值進行排序,返回一個與原數組大小相同,包含相同元素且重新排序的新數組。原數組不會被 sorted(by:)
方法修改
最復雜的形式
原數組: let names = ["Linda", "Cindy", "Benty", "Dandy"]
排序后數組:sortNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 < s2 } ) print(sortNames) // [ "Benty", "Cindy", "Dandy","Linda"]
如果第一個字符串s1小于 第二個字符串s2, 函數會返回 true,在新的數組中 s1 應該出現在 s2 前。對于字符串中的字符來說,“小于”表示“按照字母順序較晚出現”。這意味著字母 "A" 小于字母 "B" ,字符串 "Ab" 小于字符串 "Ac"。該閉包將進行字母順序排序,"Benty" 將會排在 "Cindy" 之前。
上下文推斷類型
排序閉包函數作為sorted(by:)
方法的參數傳入,Swift 可以推斷其參數和返回值的類型。sorted(by:)
方法被一個字符串數組調用,因此其參數必須是 (String, String) -> Bool
類型。這意味著 (String, String) 和 Bool 類型并不需要作為閉包表達式定義的一部分。因為所有的類型都可以被正確推斷,返回箭頭(->)和圍繞在參數周圍的括號也可以被省略:
sortNames = names.sorted(by: { s1, s2 in return s1 < s2 } )
盡管如此,你仍然可以明確寫出有著完整格式的閉包。如果完整格式的閉包能夠提高代碼的可讀性,則我們更鼓勵采用完整格式的閉包。而在 sorted(by:)
方法這個例子里,顯然閉包的目的就是排序。由于這個閉包是為了處理字符串數組的排序,因此讀者能夠推測出這個閉包是用于字符串處理的。
單表達式閉包隱式返回
sortNames = names.sorted(by: { s1, s2 in s1 < s2 } )
在這個例子中,sorted(by:)
方法的參數類型明確了閉包必須返回一個 Bool 類型值。因為閉包函數體只包含了一個單一表達式(s1 < s2),該表達式返回 Bool 類型值,因此這里沒有歧義,return 關鍵字可以省略
參數名稱縮寫
Swift 自動為內聯閉包提供了參數名稱縮寫功能,你可以直接通過 $0,$1,$2 來順序調用閉包的參數,以此類推。如果你在閉包表達式中使用參數名稱縮寫,你可以在閉包定義中省略參數列表,并且對應參數名稱縮寫的類型會通過函數類型進行推斷。in關鍵字也同樣可以被省略,因為此時閉包表達式完全由閉包函數體構成:
sortNames = names.sorted(by: { $0 < $1 } )
在這個例子中,$0和$1表示閉包中第一個和第二個 String 類型的參數。
運算符方法
實際上還有一種更簡短的方式來編寫上面例子中的閉包表達式。Swift 的 String 類型定義了關于小于號(<)的字符串實現,其作為一個函數接受兩個 String 類型的參數并返回 Bool 類型的值。而這正好與 sorted(by:) 方法的參數需要的函數類型相符合。因此,你可以簡單地傳遞一個小于號,Swift 可以自動推斷出你想使用小于號的字符串函數實現:
sortNames = names.sorted(by: <)
尾隨閉包
如果你需要將一個很長的閉包表達式作為最后一個參數傳遞給函數,可以使用尾隨閉包來增強函數的可讀性。尾隨閉包是一個書寫在函數括號之后的閉包表達式,函數支持將其作為最后一個參數調用。在使用尾隨閉包時,你不用寫出它的參數標簽。
sortNames = names.sorted() { $0 < $1 }
如果閉包表達式是函數或方法的唯一參數,則當你使用尾隨閉包時,你甚至可以把 () 省略掉:sortNames =names.sorted { $0 < $1 }
值捕獲
閉包可以在其被定義的上下文中捕獲常量或變量。即使定義這些常量和變量的原作用域已經不存在,閉包仍然可以在閉包函數體內引用和修改這些值。
舉個例子,這有一個叫做 makeIncrementor
的函數,其包含了一個叫做 incrementor
的嵌套函數。嵌套函數 incrementor()
從上下文中捕獲了兩個值,runningTotal
和 amount
。捕獲這些值之后,makeIncrementor
將 incrementor
作為閉包返回。每次調用 incrementor
時,其會以 amount
作為增量增加 runningTotal
的值。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incremented
}
makeIncrementor
返回類型為 () -> Int
。這意味著其返回的是一個函數,而非一個簡單類型的值。該函數在每次調用時不接受參數,只返回一個 Int
類型的值 makeIncrementer(forIncrement:)
函數定義了一個初始值為 0
的整型變量 runningTotal
,用來存儲當前總計數值。該值為 incrementor
的返回值
makeIncrementer(forIncrement:)
有一個 Int
類型的參數,其外部參數名為 forIncrement
,內部參數名為 amount
,該參數表示每次 incrementor
被調用時 runningTotal
將要增加的量。makeIncrementer
函數還定義了一個嵌套函數 incrementor
,用來執行實際的增加操作。該函數簡單地使 runningTotal
增加 amount
,并將其返回。
如果我們單獨考慮嵌套函數 incrementer()
,會發現它有些不同尋常:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
incrementer()
函數并沒有任何參數,但是在函數體內訪問了 runningTotal
和 amount
變量。這是因為它從外圍函數捕獲了 runningTotal
和 amount
變量的引用。捕獲引用保證了 runningTotal
和 amount
變量在調用完 makeIncrementer
后不會消失,并且保證了在下一次執行 incrementer
函數時,runningTotal
依舊存在。
閉包是引用類型
上面的例子中,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()
}
var x = 10
func doSomething() {
someFunctionWithEscapingClosure {
//逃逸閉包 需要顯式地引用 self
//調用doSomething 不會執行閉包
self.x = 100
}
someFunctionWithNonescapingClosure {
//因為不是逃逸閉包,調用doSomething 執行閉包
x = 200
}
}
doSomething()
print(x) // 打印出 "200"
completionHandlers.first?()
print(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
的函數。將閉包作為參數傳遞給函數時,你能獲得同樣的延時求值行為。
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:)
函數接受一個返回顧客名字的顯式的閉包。下面這個版本的 serve(customer:)
完成了相同的操作,不過它并沒有接受一個顯式的閉包,而是通過將參數標記為 @autoclosure
來接收一個自動閉包。現在你可以將該函數當作接受 String
類型參數(而非閉包)的函數來調用。customerProvider
參數將自動轉化為一個閉包,因為該參數被標記了@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
會讓你的代碼變得難以理解。上下文和函數名應該能夠清晰地表明求值是被延遲執行的。
如果你想讓一個自動閉包可以“逃逸”,則應該同時使用 @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(_:)
函數并沒有調用傳入的 customerProvider
閉包,而是將閉包追加到了customerProviders
數組中。這個數組定義在函數作用域范圍外,這意味著數組內的閉包能夠在函數返回之后被調用。因此,customerProvider
參數必須允許“逃逸”出函數作用域。
結束
本文內容摘自中文版 Apple 官方 Swift 教程,對于初學swift的朋友有較大幫助,下面是蘋果官方文檔鏈接和github中文文檔鏈接 官方文檔 中文文檔