Swift-逃逸閉包、自動閉包

weixin_34353714發表於2018-06-12

閉包是引用型別

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

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

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

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引數必須允許“逃逸”出函式作用域。

原文出自51Swift轉載請保留原文連結