閉包是引用類型
下面的例子中堕澄,incrementBySeven
和 incrementByTen
都是常量僵芹,但是這些常量指向的閉包仍然可以增加其捕獲的變量的值处硬。這是因為函數(shù)和閉包都是引用類型。
無論你將函數(shù)或閉包賦值給一個常量還是變量拇派,你實際上都是將常量或變量的值設(shè)置為對應(yīng)函數(shù)或閉包的引用荷辕。上面的例子中,指向閉包的引用 incrementByTen
和incrementBySeven
是一個常量件豌,而并非閉包內(nèi)容本身疮方。
這也意味著如果你將閉包賦值給了兩個不同的常量或變量,兩個值都會指向同一個閉包:
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
let incrementBySeven = makeIncrementer(forIncrement: 10)
let incrementByTen = makeIncrementer(forIncrement: 20)
incrementBySeven()
// 返回的值為10
incrementBySeven()
// 返回的值為20
incrementByTen()
// 返回的值為20
incrementByTen()
// 返回的值為40
var incrementByTenThree = makeIncrementer(forIncrement: 15)
incrementByTenThree()
// 返回的值為15
incrementByTenThree()
// 返回的值為30
逃逸閉包
當(dāng)一個閉包作為參數(shù)傳到一個函數(shù)中茧彤,但是這個閉包在函數(shù)返回之后才被執(zhí)行骡显,我們稱該閉包從函數(shù)中逃逸。當(dāng)你定義接受閉包作為參數(shù)的函數(shù)時曾掂,你可以在參數(shù)名之前標(biāo)注 @escaping惫谤,用來指明這個閉包是允許“逃逸”出這個函數(shù)的。
一種能使閉包“逃逸”出函數(shù)的方法是珠洗,將這個閉包保存在一個函數(shù)外部定義的變量中溜歪。舉個例子,很多啟動異步操作的函數(shù)接受一個閉包參數(shù)作為 completion handler许蓖。這類函數(shù)會在異步操作開始之后立刻返回蝴猪,但是閉包直到異步操作結(jié)束后才會被調(diào)用。在這種情況下膊爪,閉包需要“逃逸”出函數(shù)自阱,因為閉包需要在函數(shù)返回之后被調(diào)用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:) 函數(shù)接受一個閉包作為參數(shù)米酬,該閉包被添加到一個函數(shù)外定義的數(shù)組中沛豌。如果你不將這個參數(shù)標(biāo)記為 @escaping,就會得到一個編譯錯誤淮逻。
將一個閉包標(biāo)記為 @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"
自動閉包
自動閉包是一種自動創(chuàng)建的閉包桨啃,用于包裝傳遞給函數(shù)作為參數(shù)的表達(dá)式车胡。這種閉包不接受任何參數(shù),當(dāng)它被調(diào)用的時候照瘾,會返回被包裝在其中的表達(dá)式的值匈棘。這種便利語法讓你能夠省略閉包的花括號,用一個普通的表達(dá)式來代替顯式的閉包析命。
我們經(jīng)常會調(diào)用采用自動閉包的函數(shù)主卫,但是很少去實現(xiàn)這樣的函數(shù)。舉個例子來說鹃愤,assert(condition:message:file:line:) 函數(shù)接受自動閉包作為它的 condition 參數(shù)和 message 參數(shù)簇搅;它的 condition 參數(shù)僅會在 debug 模式下被求值,它的 message 參數(shù)僅當(dāng) condition 參數(shù)為 false 時被計算求值软吐。
自動閉包讓你能夠延遲求值瘩将,因為直到你調(diào)用這個閉包,代碼段才會被執(zhí)行凹耙。延遲求值對于那些有副作用(Side Effect)和高計算成本的代碼來說是很有益處的姿现,因為它使得你能控制代碼的執(zhí)行時機(jī)。下面的代碼展示了閉包如何延時求值肖抱。
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
的第一個元素被移除了,不過在閉包被調(diào)用之前虐沥,這個元素是不會被移除的熊经。如果這個閉包永遠(yuǎn)不被調(diào)用,那么在閉包里面的表達(dá)式將永遠(yuǎn)不會執(zhí)行欲险,那意味著列表中的元素永遠(yuǎn)不會被移除镐依。請注意,customerProvider
的類型不是 String天试,而是 () -> String
槐壳,一個沒有參數(shù)且返回值為 String 的函數(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ù)接受一個返回顧客名字的顯式的閉包。下面這個版本的 serve(customer:)
完成了相同的操作带兜,不過它并沒有接受一個顯式的閉包枫笛,而是通過將參數(shù)標(biāo)記為 @autoclosure
來接收一個自動閉包。現(xiàn)在你可以將該函數(shù)當(dāng)作接受 String 類型參數(shù)(而非閉包)的函數(shù)來調(diào)用刚照。customerProvider
參數(shù)將自動轉(zhuǎ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 會讓你的代碼變得難以理解。上下文和函數(shù)名應(yīng)該能夠清晰地表明求值是被延遲執(zhí)行的
如果你想讓一個自動閉包可以“逃逸”啊楚,則應(yīng)該同時使用 @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ù)組中。這個數(shù)組定義在函數(shù)作用域范圍外颜价,這意味著數(shù)組內(nèi)的閉包能夠在函數(shù)返回之后被調(diào)用涯保。因此,customerProvider
參數(shù)必須允許“逃逸”出函數(shù)作用域拍嵌。