Swift-逃逸閉包、自動閉包
閉包是引用型別
下面的例子中,incrementBySeven
和 incrementByTen
都是常量,但是這些常量指向的閉包仍然可以增加其捕獲的變數的值。這是因為函式和閉包都是引用型別。
無論你將函式或閉包賦值給一個常量還是變數,你實際上都是將常量或變數的值設定為對應函式或閉包的引用。上面的例子中,指向閉包的引用 incrementByTen
和incrementBySeven
是一個常量,而並非閉包內容本身。
這也意味著如果你將閉包賦值給了兩個不同的常量或變數,兩個值都會指向同一個閉包:
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
逃逸閉包
當一個閉包作為引數傳到一個函式中,但是這個閉包在函式返回之後才被執行,我們稱該閉包從函式中逃逸。當你定義接受閉包作為引數的函式時,你可以在引數名之前標註 @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
屬性。@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
引數必須允許“逃逸”出函式作用域。
相關文章
- 閉包
- 閉包 | 淺談JavaScript閉包問題JavaScript
- 【集合論】關係閉包 ( 關係閉包求法 | 關係圖求閉包 | 關係矩陣求閉包 | 閉包運算與關係性質 | 閉包複合運算 )矩陣
- 閉包是什麼?怎麼形成一個閉包?為什麼使用閉包?
- Swift 閉包Swift
- golang 閉包Golang
- 「閉包」攻略
- PHP 閉包PHP
- JavaScript閉包JavaScript
- JavaScript - 閉包JavaScript
- Golang閉包Golang
- JavaScript 閉包JavaScript
- 理解“閉包”
- 什麼是閉包,閉包的優缺點?
- 什麼是閉包?閉包的作用是什麼?
- js閉包及閉包的經典使用場景JS
- [JavaScript閉包]Javascript閉包的判別,作用和示例JavaScript
- C#閉包C#
- 筆記:閉包筆記
- 亂談閉包
- 閉包的起源
- 函式閉包函式
- JavaScript-閉包JavaScript
- JS閉包ClosureJS
- JavaScript 的閉包JavaScript
- 理解JavaScript 閉包JavaScript
- JavaScript之閉包JavaScript
- Groovy閉包理解
- 閉包作用域
- 再讀閉包
- 閉包問題
- go 閉包函式Go函式
- js閉包的理解JS
- 閉包,是真的美
- js中的閉包JS
- 閉包詳解一
- 淺談js閉包JS
- 什麼是閉包?