標籤:version 函數 多個 範圍 尋找 消失 span [] amount
閉包是自包含的功能塊,可以在代碼中傳遞和使用。Swift閉包與c和oc中的block類似,其他語言用lambdas。
閉包可以擷取和儲存指向在閉包內部定義的任何常量和變數,這就是所謂的封閉常量和變數,swift為你處理所有的捕捉的記憶體管理。
注意:如果你對捕捉的概念不熟悉,不用擔心,在Capture Value中會詳情的說明。
已經在Function中介紹的全域方法和嵌套方法,實際上是Closures的一個中特殊例子。Closures採用三種形式之一:
- 全域函數是一種 有個名字但是沒有捕獲任何值的閉包函數。
- 嵌套函數是一種 有一個函數名並且可以從他們的閉包函數中擷取到值的閉包。
- 閉包運算式是使用輕量級文法編寫的閉包,可以從周圍環境捕獲值。
Swift的閉包運算式有一種乾淨、清晰的風格,在常見的情境中使用了鼓勵簡短、無規則的文法的最佳化。這些最佳化包括:
從上下文推斷參數和傳回值類型
來自間單運算式閉包的隱式返回
速記參數名稱
後關閉的文法
1.閉包的運算式
嵌套函數,是一種方便命名和定義自包函數塊的方法,作為一個大函數的一部分。然而,有時候編寫一個間斷的不帶完整的聲明和命名的函數結構是很有用的,尤其是在需要用函數作為一個或者多個函數或者方法的參數的時候。
閉包運算式一種是簡潔、集中的文法編寫的內聯閉包的方法。閉包運算式提供了一些文法最佳化,可以在短表單中編寫閉包,而不會失去清晰或意圖。下面閉包運算式的例子說明了這些最佳化,通過重新定義sorted(by:)函數,每個運算式都以更簡潔的方式表達相同的功能。
(1)排序方法
Swift標準庫提供一個sroted(by:)的方法,基於你提供的排序閉包輸出,給一個已知類型的數組排序。一旦這個排序完成,sorted(by:)方法返回一個新的按照正確順序排過序的數組,數組的類型和大小都與舊的數組一致。原始的數組不會被修改。
下面閉包運算式的例子用sorted(by:)方法為一個字串數組按照字母順序排序。排序(通過:)方法接受一個閉包,該閉包接受與數組內容相同類型的兩個參數,並返回Bool值,以確定一旦值被排序,第一個值是否應該出現在第二個值之前或之後。如果第一個值出現在第二個值之前,那麼排序結束需要返回true,否則將返回false。
這個例子是對字串數組進行排序的,所這個排序的閉包需要函數類型為(String, String) -> Bool.
提供排序閉包的一個方法是寫一個普通正確類型的函數,把這個函數對坐sorted(by:)方法的參數傳遞進去。
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]func backward(_ s1: String, _ s2: String) -> Bool { return s1 > s2}var reversedNames = names.sorted(by: backward)// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
如果第一個字元(S1)比第二個字串(S2)大,backward(_:_:)這個函數會返回true,說明在排序數組中S1應該拍在S2前面。
(2)閉包運算式的文法
{ (parameters) -> return type in statements}
在閉包運算式中的文法中,參數可以用輸入輸出參數,但他們不能有預設的值。當你具名引數變數的時候,變數參數可以使用,元組也可以作為閉包運算式的參數。
前面提到的 backward(_:_:)函數可以用下面的方法來表示:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2})
注意的是,在函數 backward(_:_:)的聲明與內聯閉包
函式宣告參數和傳回值類型是一樣的。這個兩個例子中,都寫成:(s1: String, s2: String) -> Bool。然而,在內嵌函式閉包表達中,參數和傳回值都寫在花括弧內,而不是外部。
閉包函數體以“in”關鍵字為開始。這個關鍵字表示這個閉包的參數和傳回值類型已經完成,而閉包體剛剛開始。
如果閉包體很短,可以寫在同一行裡:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
(2)從上下文中推斷類型
因為閉包函數作為一個參數傳給一個方法,Swift可以推斷這個參數的類型以及這個傳回值的類型。sprted(by:)這個方法在字串數組調用的時候,它的參數必須(String, String)->Bool 的函數類型。意味著(String, String)和 Bool 類型不需要作為閉包運算式定義的一部分。因為所有的類型都是推斷,返回箭頭(->)和參數名字的括弧也可以省略。
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
當將閉包傳給一個函數或者方法作為內聯閉包表達時,通常都是可以推斷出參數的類型和傳回值類型的。
當然,如果你想的話,仍然讓你的類型清晰一點,如果為了你的代碼讀起來避免模淩兩可的時候還是鼓勵大家那麼坐的。
(3)從簡單閉包運算式返回一個精確的值
簡單運算式的閉包,在他們的運算式聲明中省略return關鍵字,也可以精確返回一個結果。
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
這裡,sorted(by:)方法的參數的函數類型讓這個方法很清晰明白,這個閉包肯定返回一個Bool值。
(4)縮短參數名
SSwift自動為內聯閉包提供了簡短的參數名稱,可以用來引用閉包參數的值,名稱為$ 0、$ 1、$ 2,等等。
如果你在閉包運算式中用這些短參數的話,你可以在定義閉包的時候,省略參數列表。短參數的名稱的個數和類型可以從指定的參數類型來推斷出來。in關鍵字也可以被忽略,因為閉包運算式完全由它的閉包體組成。
reversedNames = names.sorted(by: { $0 > $1 } )
這裡,$0,$1引用了閉包第一個和第二String類型的參數。
(5)操作方法
上面的閉包閉包運算式中還有一個更簡短的方法。Swift的String類型定義了比操作符(>)做為一種具有兩個字串類型的參數和返回一個bool類型值的方法。因此,你可以簡單地傳一個比操作符,Swift會自動推斷出你想用它的string-specific功能。
reversedNames = names.sorted(by: >)
想瞭解更多操作方法,請看“ Operator Methods”。
2.尾隨閉包
如果你需要給一個函數傳入一個閉包運算式作為函數最後的參數,而且這個閉包運算式很長的話, 你可以用尾隨閉包來代替。尾隨閉包寫在函數調用的括弧後面,但是它仍然是這個函數的參數。當你使用尾隨閉包文法的時候,你不需要將閉包的參數標籤寫為函數調用的一部分。
func someFunctionThatTakesAClosure(closure: () -> Void) { // function body goes here} // Here‘s how you call this function without using a trailing closure: someFunctionThatTakesAClosure(closure: { // closure‘s body goes here}) // Here‘s how you call this function with a trailing closure instead: someFunctionThatTakesAClosure() { // trailing closure‘s body goes here}
在閉包運算式文法部分中的字串排序閉包,可以寫在sroted(by:)方法括弧外面作為尾隨閉包:
reversedNames = names.sorted() { $0 > $1 }
如果一個閉包運算式作為一個函數或者方法的唯一參數,而且你提供的這個運算式作為尾隨閉包,則當你調用這個方法的時候,你不需要在函數和方法名的後面寫()。
reversedNames = names.sorted { $0 > $1 }
當閉包比較長以至於在內嵌函式中一行寫不下時,用尾隨閉包是相當有用的。例如,Swift 的數群組類型有個map(_:)方法,它用閉包運算式作為唯一的參數。數組中的每個元素都會調用一次這個閉包,並且為每個元素返回一個可替代的值(可能是其他類型的)。映射的性質和傳回值的類型留給閉包來指定。
在要求為數組的每個元素提供閉包後,map(_:)方法返回一個包含所有新映射值的數組,順序與原資料的順序一致。
這裡就是說明,你如果用一個帶有尾隨閉包的map(_:)方法把Int數組轉成String字元的數組。用數組[16,58,510]建立一個["OneSix‘,"FiveEight","FiveOneZero"]新的數組。
let digitNames = [ 0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four", 5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"]let numbers = [16, 58, 510]
上面的代碼建立了一個關於數字和數字英文名的字典類型的映射。還定義了一個用於轉化成字串的數字數組。
你現在可以用numbers數組去建立一個String類型值的數組,通過給數組map(_:)方法的尾隨閉包傳入一個閉包運算式。
let strings = numbers.map { (number) -> String in var number = number var output = "" repeat { output = digitNames[number % 10]! + output number /= 10 } while number > 0 return output}// strings is inferred to be of type [String]// its value is ["OneSix", "FiveEight", "FiveOneZero"]
在map(_:)方法中數組的每個元素都會調用一次閉包運算式。你不需要制定閉包輸入參數的類型,即number,因為這個類型可以根據數組元素的值推斷出來。
在這個例子中,變數 number 是有閉包參數number初始化的值,所以這個值可以在閉包體中修改。(方法和閉包的參數始終都是常量)。閉包運算式也制定了一個傳回型別String,為了表明在映射輸出的數組中用這個類型排序。
這個閉包運算式被調用的時候會構建一個output的字串。它通過使用剩餘的運算子來計算最後一個數字(number % 10),然後用這個數字在digitNames的字典中,尋找是否有匹配的字串。
注意:對digitNames字典的下標的調用後面跟著一個驚嘆號(!),因為字典子指令碼返回一個可選的值,以表明如果鍵不存在,字典尋找就會失敗。在上面的樣本中,可以保證number % 10始終是數字字典的一個有效下標鍵,因此使用驚嘆號來強制解壓儲存在下標可選傳回值中的字串值。
從digitNames字典中取得的字串會添加在output前面,有效構建了這個數字轉換後的字串。
變數number被10整除,因為它是一個整數,它在除法中是圓形的,所以,16變成了1,58變成了5,510變成了51.
這個過程一直重複知道這個number等於0,與此同時,output字串也從閉包中返回,並添加到了map(_:) 方法的輸出數組中。
在上述尾隨閉包文法中,當函數的閉包一提供,就可以封裝閉包功能,不需要在map(_:)方法的()內部封裝整個閉包。
3.捕獲值
一個閉包可以根據它定義的上下文來擷取一個常量和變數。閉包可以在它的閉包體中引用和修改這些常量和變數的值,即使在最初定義的常量和變數的範圍不再存在。
在Swift中,可以捕獲值的閉包最簡單形式是嵌套函數,在閉包體中編寫另外一個方法。一個嵌套函數可以擷取到在它外面函數的參數,並且可以擷取這些外函數定義的常量和變數。
這個例子就是一個叫makeIncrementer函數,它包含了一個嵌套函數incrementer。這個嵌套函數incrementer()函數可以從上下文中擷取到兩個值runningTotal和amount。擷取到這兩個值後,makeIncrementer將incrementer作為閉包返回,每次調用時遞增。
func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer}
makeIncrementer的傳回型別是()->Int,這意味著它返回的是一個函數,而不是一個值。這個返回的函數沒有參數,每次調用的時候返回Int值。
makeIncrementer(forIncrement:)函數定義一個runningTotal的整型變數,儲存incrementer中正在運行總數的值,並且返回。這個整數初始化為0.
makeIncrementer(forIncrement:)函數有一個Int類型的參數,參數標籤為forIncrement,參數名為amount。當調用incrementer這個方法時,傳入參數指定runningTotal每次返回時增加了多少。在makeIncrementer函數中定義個了一個嵌套函數叫做incrementer,這個嵌套函數的作用是增加。這個嵌套函數只是簡單地把amount與runningTotal的值相加,並返回相加的結果。
如果單獨考慮這個嵌套函數,會覺得很奇怪:
func incrementer() -> Int { runningTotal += amount return runningTotal}
incrementer()函數沒有任何的參數,它仍然在閉包體中引用runningTotal和amount。它從上下文函數中捕獲了runnintTotal和amoun值,並在自己的函數體中使用了它們。通過引用擷取值可以確保runningTotal和amount不會消失當makeIncrementer調用結束後,而且確保下一次incrementer函數調用的時候,runningTotal是有效。
注意:
作為一種最佳化,如果不通過閉包來改變值,那麼Swift可以捕獲並儲存一個值的副本,如果在建立了閉包之後值沒有發生突變,則可以儲存該值的副本。
Swift還處理在不再需要時處理變數的所有記憶體管理。
下面是makeIncrementer的調用:
let incrementByTen = makeIncrementer(forIncrement: 10)
這個樣本設定了一個名為incrementByTen的常量,以引用一個遞增函數,每次調用它時,它都會為它的runningTotal變數增加10個變數如果多次調用這個函數,那麼結果如下:
incrementByTen()// returns a value of 10incrementByTen()// returns a value of 20incrementByTen()// returns a value of 30
如果你再建立一個遞增函數,它將有它自己的儲存引用到一個新的、獨立的runningTotal變數:
let incrementBySeven = makeIncrementer(forIncrement: 7)incrementBySeven()// returns a value of 7
再一次調用incrementerByTen,會繼續增加它自己的runningTotal變數,不會影響incrementBySeven的變數。
incrementByTen()// returns a value of 40
注意:如果你給一個類對象設定了閉包屬性,而且這個閉包通過引用這個執行個體或者它的成員擷取到這個執行個體,你會在閉包和執行個體中建立強的循環參考。
4.閉包是參考型別
上述的例子中,incrementBySeven和incrementByTen是常量,但是閉包常量的引用依然可以增加捕獲到的runningTotal變數的值。這是因為函數和閉包都是參考型別。
無論什麼時候,你分配一個函數或者閉包給一個常量或者變數,你實際上是把這個常量和變數指向了函數或者閉包。上面的例子,incrementByTen指的是一個常量,不是指閉包的內容。
它意味著如果你分配一個閉包給兩個不同的變數或者常量,這兩個常量或者變數指向相同的閉包:
let alsoIncrementByTen = incrementByTenalsoIncrementByTen()// returns a value of 50
5.逃離閉包
如果一個閉包當成一個函數的參數傳入時,這個閉包稱之為逃離函數,但是它是在函數返回時才調用的。當你聲明一個函數,函數的其中一個參數是閉包,你可以在參數類型前面寫上“@escaping”表明閉包是允許逃離的。
還有一個閉包可以escape的方法是:通過儲存在一個定義在函數之外的變數上。例如,許多啟動非同步作業的函數採用閉包參數作為完成處理常式。這個函數在操作開始時返回,但是閉包不會被調用知道這個操作完成--閉包需要轉義,稍後調用。例如:
var completionHandlers: [() -> Void] = []func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler)}
someFunctionWithEscapingClosure(_:)函數參數類型是閉包,並把它添加到在函數外的一個數組中。如果你不在函數的參數上標記“ @escaping
”,會提示編輯時錯誤。
通過@escape來控制閉包意味著您必須在閉包中顯式地引用自顯。例如,在下面的代碼中,閉包傳遞給someFunctionWithEscapingClosure(_:)是一種escape closure,這意味著它需要顯式地引用self。相比之下,閉包傳遞給someFunctionWithNonescapingClosure(_:)是一個nonescaping closure,這意味著它可以指自我暗示。
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)// Prints "200" completionHandlers.first?()print(instance.x)// Prints "100"
6.自動閉包
自動閉包是一個閉包,它是自動建立的、用來作為函數參數的運算式。調用的時候,不需要傳入任何的參數,返回這個運算式的值。這種句法方便可以讓您在函數的參數周圍省略大括弧,而不是顯式的closur,而是編寫一個正常的運算式。
通過自動閉包調用函數是很正常的,但是實現這種方法並不常見。例如: assert(condition:message:file:line:)函數用自動閉包作為condition和message的參數,condition這個參數只有在編譯debug時才調用,而message只有在condition為false時才調用。
一個自動閉包讓你延遲評估,因為內部代碼不會執行知道你調用這個閉包。延遲評估對有副作用或者計算費用很昂貴的代碼很有用,因為它讓你控制碼什麼時候評估。下面的代碼顯示了一個閉包如果延遲評估。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]print(customersInLine.count)// Prints "5" let customerProvider = { customersInLine.remove(at: 0) }print(customersInLine.count)// Prints "5" print("Now serving \(customerProvider())!")// Prints "Now serving Chris!"print(customersInLine.count)// Prints "4"
儘管customersInLine數組的第一個元素在閉包的內部已經被移除,但是數組的元素並不會真的移除粗肥閉包真的被調用了。如果閉包沒有被調用,這個閉包的運算式不會被評估,也就是說數組中的元素永遠不會被移除。注意,customerProvider的類型不是String而是()->String--一個沒有參數但是有傳回值的函數。
你可以擷取相同的延遲效果,當你傳入一個閉包作為一個函數的參數時:
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]func serve(customer customerProvider: () -> String) { print("Now serving \(customerProvider())!")}serve(customer: { customersInLine.remove(at: 0) } )// Prints "Now serving Alex!"
上述的serve(customer:)函數採用這個精確的閉包,它返回一個使用者的名字。下面那個版本的serve(customer:)執行相同的操作,但不是用精確的閉包,而是採用帶有 @autoclosure的
自動閉包為參數作為參數的類型 。現在你可以調用這個函數用String作為閉包。
注意:過度使用自動閉包會讓你代碼看起來費力。
如果你想用一個自動閉包來允許逃逸,用 @autoclosure
and @escaping屬性。
// customersInLine is ["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.")// Prints "Collected 2 closures."for customerProvider in customerProviders { print("Now serving \(customerProvider())!")}// Prints "Now serving Barry!"// Prints "Now serving Daniella!"
Swift-- 閉包