Swift 程式語言 — Closures (2)
讓我們繼續深入研究閉包中知識吧!
前言:
前一篇關於 Closures 的文章應該讓大家對於閉包有初步的了解,也了解到了尾隨閉包的用途及用法。這次我們將帶領大家了解到更多有關閉包的知識以及閉包的種類。
⎮ 捕獲值 (Capturing Values)
閉包可以從定義其上下文中捕獲常數以及變數。然後閉包可以飲用和修改其中的常數和變數的值,即使定義時的常數跟變數的原始範圍已經不存在。
在 Swift 中,可以捕獲值的最簡單的閉包是嵌套函數,寫在另一個函數體中。嵌套函數可以捕獲任何外部函數的參數,也能夠捕獲任何定義在外部函數中的常數和變數。
這邊我們定義一個叫做 makeIncrementer
函數範例:
我們在 makeIncrementer
函數中也定義了一個 incrementer
嵌套函數,並且該函數類型為 ()-> Int
,並且會捕獲 runningTotal
和 amount
的值,並且最後加總後返回 runningTotal
。 而當我們 makeIncrementer
執行結束後會將我們的 incrementer
函數作為返回值傳出。
但假如我們只單看這個內嵌函數 incrementer
時,你會發現不同尋常:
你會發現雖然該函數類型為 () -> Int
,但是,其中的 runningTotal
和 amount
都不來自身,而是來自 makeIncrementer
這個函數主體。當調用 makeIncrementer
結束時通過引用捕獲來確保不會消失,並確保了在下次再次調用 incrementer
時, runningTotal
將繼續增加。
incrementmenter
函數沒有任何參數,但它引用了函數主體內的 runningTotal
和 amount
。 它通過捕獲對周圍函數的 runningTotal
和 amount
的引用並在其自己的函數體中使用它來實現此目的。 通過引用捕獲可確保在調用 makeIncrementer
結束時 runningTotal
和 amount
不會消失,並確保在下次調用 incrementmenter
函數時 runningTotal
可用。
作為優化,如果一個值沒有改變或者在閉包的外面,Swift可能會使用這個值的拷貝而不是捕獲。
Swift也處理了變數的內存管理操作,當變數不需要時會被釋放。
接著讓我們來使用 makeIncrementer
作為範例:
這邊我們定義了一個叫 incrementByTen
的常數,該常數指向一個每次調用會加 10 的函數。這邊讓我們執行其中的函數:
可以看到 incrementByTen
其中會有一個對於 runningTotal
的引用,使其能夠不斷增加其變數的值。
而當我們再次創建一個稱為 incrementByFive
,他也會有一個新的獨立的 runningTotal
變數的引用:
並且 incrementByTen
與 incrementByFive
兩者不會互相影響。
如果你分配了一個閉包給類實例的屬性,並且閉包通過引用該實例或者它的成員來捕獲實例,你將在閉包和實例間建立一個循環強引用。
⎮ 閉包是引用類型
我們上述範例中, incrementBySeven
和 incrementByTen
是常數,但是兩者都指向的閉包仍可以增加已捕獲的變量 runningTotal
的值。這是因為函數和閉包都是引用類型 (Reference Types)。
無論你什麼時候賦值一個函數或者閉包給常數或者變數,實際上都是將其設置為對函數和閉包的引用。這上面這個例子中,閉包選擇 incrementByTen
指向一個常數,而不是閉包它自身的內容。
這也意味著你賦值一個閉包到兩個不同的常量或變量中,這兩個常量或變量都將指向相同的閉包:
可以看到我們 alsoIncrementByTen
也是指向與 incrementByTen
同一個閉包,也代表著其對於 runningTotal
的變數引用也是同一個。
⎮ 逃逸閉包 (Escaping Closures)
當閉包作為一個參數傳遞給一個函數的時候,我們就說這個閉包逃逸了,因為它可以在函數返回之後被調用。當你宣告一個接受閉包作為參數的函數時,你可以在其參數前標記 @escaping
表示閉包是允許逃逸的。
閉包可以逃逸的一種方法是被儲存在定義於函數外的變數裡。比如說,很多函數接收閉包參數來作為啟動異步任務的回調。函數在啟動後返回,但是閉包要直到任務完成 — 閉包需要逃逸,以便於稍後調用。舉例來說:
函數 someFunctionWithEscapingClosure
接收一個閉包作為參數並且添加它到函數外部的數組裡。如果你不標記函數的參數為 @escaping
,你就會遇到編譯錯誤。
讓閉包 @escaping
意味著你必須在閉包中顯式地引用 self
,比如說,下面的程式碼中,傳給 someFunctionWithEscapingClosure
的閉包是一個逃逸閉包,也就是說它需要顯式地引用 self
。相反,傳給 someFunctionWithNonescapingClosure
的閉包是非逃逸閉包,也就是說它可以隱式地引用 self
。
接著讓我們實例化 SomeClass
並且運行其中的逃逸閉包以及非逃逸閉包,並且執行其中的 doSomething
:
你會發現目前實例中的 x
目前被非逃逸閉包更改為 200,而另一個逃避閉包將其閉包 append 到函數外的 completionHandlers
的數組中,而切下來我們調用其數組第一個(也是唯一一個),並執行該函數:
這時你會發現我們會執行到 someFunctionWithEscapingClosure
中所傳出的函數,並且把實例中的 x
的值更改為 100。
⎮ 自動閉包 (Autoclosures)
自動閉包顧名思義是一種自動創建的閉包,其用於包裝作為參數傳遞給函數的表達式。它不接受任何參數,而當他被調用時,它會返回包含在其中表達式的值。這個語法的好處在於通過寫普通表達式代替顯式閉包而使你省略包圍函數參數的括號。
自動閉包允許你延遲處理,因為直到你調用閉包之前,內部程式碼不會運行。延遲處理對於有副作用或是計算成本高的程式碼非常有用,因為它能讓控制何時該處理程式碼。下面我們來演示如何進行延遲處理:
首先我們建立一個 customerInLine
數組,並且印出其 count
:
接著我們宣告一個 customerProvider
常數,其內容執行 remove
方法的閉包表達式:
儘管 customersInLine
數組的第一個元素在閉包內被刪除,但是在實際調用閉包之前不會刪除數組中的元素。所以如果從不調用閉包,則永遠不會計算閉包內的表達式。
請注意,
customerProvider
的類型不是String
,而是() -> String
函數。
當你將閉包作為參數傳遞給函數時,你會得到與延遲執行相同的行為:
上面程式碼中的 serve(customer:)
函數採用顯式閉包來返回客戶名稱。而下面的 serve(customer:)
版本執行相同的操作,但是不是採用顯式閉包,而是透過 @autoclosure
標記其參數的類型來獲取自動閉包。
現在你可以像調用 String 參數而不是閉包那樣調用函數。參數會自動被轉換為閉包,因為 customerProvider
參數的類型被標記為 @autoclosure
。
濫用自動閉包會導致你的程式碼難以理解。上下文和函數名應該標示清楚處理是延遲的。
如果你想要允許自動閉包同時也是逃逸閉包,就同時使用 @autoclosure
和 @escaping
標記。
後記:
那麼我們閉包的文章就到這邊結束了,經過函數以及閉包的洗禮後,相信大家對於程式的理解又更進一步了,若是對於閉包的概念還不夠熟係也別害怕,等到遇到的時候再回頭來看,說不定能更能加深你的印象,希望大家對於這幾種閉包的概念有更深刻的印象,我們下篇文章見。