★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
?微信公眾號:山青詠芝(shanqingyongzhi)
?博客園地址:山青詠芝(https://www.cnblogs.com/strengthen/)
?GitHub地址:https://github.com/strengthen/LeetCode
?原文地址:https://www.cnblogs.com/strengthen/p/9728063.html?
?如果鏈接不是山青詠芝的博客園地址,則可能是爬取作者的文章。
?原文已修改更新!強烈建議點擊原文地址閱讀!支持作者!支持原創!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
閉包是自包含的功能塊,可以在代碼中傳遞和使用。Swift中的閉包類似于C和Objective-C中的塊以及其他編程語言中的lambdas。
閉包可以從定義它們的上下文中捕獲和存儲對任何常量和變量的引用。這被稱為關閉那些常量和變量。Swift為您處理捕獲的所有內存管理。
注意
如果您不熟悉捕獲的概念,請不要擔心。下面在捕獲值中詳細解釋了它。
全球和嵌套函數,如推出的功能,實際上是封閉的特殊情況。閉包采用以下三種形式之一:
- 全局函數是具有名稱但不捕獲任何值的閉包。
- 嵌套函數是具有名稱的閉包,可以從其封閉函數中捕獲值。
- Closure表達式是一種未命名的閉包,用輕量級語法編寫,可以從周圍的上下文中捕獲值。
Swift的閉包表達式具有干凈,清晰的風格,優化可以在常見場景中鼓勵簡潔,無雜亂的語法。這些優化包括:
- 從上下文中推斷參數和返回值類型
- 單表達式閉包的隱式返回
- 速記參數名稱
- 尾隨閉包語法
關閉表達式
嵌套函數(在嵌套函數中引入)是一種方便的方法,可以將自包含的代碼塊命名和定義為更大函數的一部分。但是,在沒有完整聲明和名稱的情況下編寫類似函數的構造的更短版本有時是有用的。當您使用將函數作為其一個或多個參數的函數或方法時,尤其如此。
Closure表達式是一種以簡短,集中的語法編寫內聯閉包的方法。Closure表達式提供了幾種語法優化,用于以縮短的形式編寫閉包,而不會丟失清晰度或意圖。下面的閉包表達式示例通過sorted(by:)
在幾次迭代中細化該方法的單個示例來說明這些優化,每個迭代以更簡潔的方式表達相同的功能。
排序方法
Swift的標準庫提供了一個名為的方法sorted(by:)
,它根據您提供的排序閉包的輸出對已知類型的值數組進行排序。完成排序過程后,該sorted(by:)
方法返回一個與舊數組相同類型和大小的新數組,其元素按正確的排序順序排列。該sorted(by:)
方法不會修改原始數組。
下面的閉包表達式示例使用該sorted(by:)
方法以String
反向字母順序對值數組進行排序。這是要排序的初始數組:
- let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
該sorted(by:)
方法接受一個閉包,該閉包接受與數組內容相同類型的兩個參數,并返回一個Bool
值,以說明一旦值被排序后第一個值是出現在第二個值之前還是之后。true
如果第一個值應出現在第二個值之前,則需要返回排序閉包,false
否則返回。
這個例子是對String
值數組進行排序,因此排序閉包需要是類型的函數。(String,?String)?->?Bool
提供排序閉包的一種方法是編寫正確類型的普通函數,并將其作為參數傳遞給sorted(by:)
方法:
- 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
在排序數組之前出現。對于字符串中的字符,“大于”表示“在字母表后面出現”。這意味著字母"B"
“大于”字母"A"
,字符串"Tom"
大于字符串"Tim"
。這給出了一個反向字母排序,"Barry"
放在之前"Alex"
,依此類推。
然而,這是一種相當冗長的方式來編寫本質上是單表達式函數()。在這個例子中,最好使用閉包表達式語法內聯編寫排序閉包。a?>?b
閉包表達式語法
Closure表達式語法具有以下一般形式:
- { (parameters) -> return type in
- statements
- }
該參數在封閉表達式語法可以在輸出參數,但是他們不能有一個默認值。如果命名variadic參數,則可以使用變量參數。元組也可以用作參數類型和返回類型。
下面的示例顯示了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 } )
這說明對sorted(by:)
方法的整體調用保持不變。一對括號仍然包裝該方法的整個參數。但是,該參數現在是內聯閉包。
從上下文中推斷類型
因為排序閉包作為參數傳遞給方法,所以Swift可以推斷出它的參數類型以及它返回的值的類型。該sorted(by:)
方法是在一個字符串數組上調用的,因此它的參數必須是一個類型的函數。這意味著不需要將和類型作為閉包表達式定義的一部分來編寫。因為可以推斷出所有類型,所以也可以省略返回箭頭()和參數名稱周圍的括號:(String,?String)?->?Bool
(String,?String)
Bool
->
- reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
在將閉包作為內聯閉包表達式傳遞給函數或方法時,始終可以推斷出參數類型和返回類型。因此,當閉包用作函數或方法參數時,您永遠不需要以最完整的形式編寫內聯閉包。
盡管如此,如果您愿意,仍然可以使類型顯式化,如果它避免了代碼讀者的歧義,則鼓勵這樣做。在該sorted(by:)
方法的情況下,封閉的目的是從分類發生的事實中清楚的,并且讀者可以認為封閉可能與String
值一起工作是安全的,因為它有助于分類。一串字符串。
單表達式閉包的隱式返回
單表達式閉包可以通過return
從聲明中省略關鍵字來隱式返回單個表達式的結果,如上一個示例的此版本中所示:
- reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
這里,sorted(by:)
方法參數的函數類型清楚地表明Bool
閉包必須返回一個值。因為閉包的主體包含一個返回值的表達式(),所以沒有歧義,可以省略關鍵字。s1?>?s2
Bool
return
速記參數名稱
雨燕自動提供速記參數名內聯閉包,它可以使用的名稱,指的是關閉的參數值$0
,$1
,$2
,等等。
如果在閉包表達式中使用這些簡寫參數名稱,則可以從其定義中省略閉包的參數列表,并且將從期望的函數類型推斷縮寫參數名稱的數量和類型。的in
關鍵字也可以被省略,因為封閉件表達是由完全其身體的:
- reversedNames = names.sorted(by: { $0 > $1 } )
在這里,$0
并$1
參考閉包的第一個和第二個String
參數。
操作員方法
實際上有一種更短的方式來編寫上面的閉包表達式。Swift的String
類型將其大于運算符(>
)的字符串特定實現定義為具有兩個類型參數的方法String
,并返回type的值Bool
。這與方法所需的方法類型完全匹配sorted(by:)
。因此,您可以簡單地傳入大于運算符,Swift將推斷您要使用其特定于字符串的實現:
- reversedNames = names.sorted(by: >)
欲了解更多有關操作方法,請參閱操作方法。
尾隨閉包
如果需要將閉包表達式作為函數的最終參數傳遞給函數,并且閉包表達式很長,則將其寫為尾隨閉包可能很有用。在函數調用的括號之后寫入尾隨閉包,即使它仍然是函數的參數。使用尾隨閉包語法時,不要將閉包的參數標簽寫為函數調用的一部分。
- 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
- }
上面的Closure Expression Syntax部分中的字符串排序閉包可以sorted(by:)
作為尾隨閉包寫在方法的括號之外:
- reversedNames = names.sorted() { $0 > $1 }
如果提供閉包表達式作為函數或方法的唯一參數,并且您將該表達式作為尾隨閉包提供,則()
在調用函數時,不需要在函數或方法的名稱后面寫一對括號:
- reversedNames = names.sorted { $0 > $1 }
當閉包足夠長以至于無法將其內聯寫入單行時,尾隨閉包最有用。作為一個例子,Swift的Array
類型有一個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
使用closure的number
參數值初始化變量,以便可以在閉包體內修改該值。(函數和閉包的參數總是常量。)閉包表達式還指定了返回類型String
,以指示將存儲在映射的輸出數組中的類型。
閉包表達式構建一個output
每次調用時調用的字符串。它number
使用余數運算符()計算最后一位數,并使用該數字在字典中查找相應的字符串。閉包可用于創建任何大于零的整數的字符串表示。number?%?10
digitNames
注意
對digitNames
字典下標的調用后跟一個感嘆號(!
),因為字典下標返回一個可選值,表示如果該鍵不存在,字典查找可能會失敗。在上面的示例中,保證始終是字典的有效下標鍵,因此使用感嘆號強制解包存儲在下標的可選返回值中的值。number?%?10
digitNames
String
從檢索到的字符串digitNames
辭典被添加到前面的output
,有效地建立反向一數目的字符串版本。(表達式給出for?,for?和for的值。)number?%?10
6
16
8
58
0
510
number
然后將變量除以10
。因為它是一個整數,所以它在分割期間向下舍入,因此16
變為1
,58
變為5
,510
變為51
。
重復該過程直到number
等于0
,此時output
字符串由閉包返回,并通過該map(_:)
方法添加到輸出數組。
在上面的示例中使用尾隨閉包語法在閉包支持的函數之后立即巧妙地封裝了閉包的功能,而無需將整個閉包包裝在map(_:)
方法的外括號中。
捕捉價值觀
閉包可以從定義它的周圍上下文中捕獲常量和變量。然后閉包可以引用并修改其體內的常量和變量的值,即使定義常量和變量的原始范圍不再存在。
在Swift中,可以捕獲值的最簡單形式的閉包是嵌套函數,寫在另一個函數體內。嵌套函數可以捕獲其外部函數的任何參數,還可以捕獲外部函數中定義的任何常量和變量。
這是一個調用函數的示例makeIncrementer
,其中包含一個名為的嵌套函數incrementer
。嵌套incrementer()
函數捕獲兩個值,runningTotal
并且amount
,從它的周圍環境。捕獲這些值后,incrementer
將makeIncrementer
作為一個閉包返回runningTotal
,amount
每次調用時它都會遞增。
- func makeIncrementer(forIncrement amount: Int) -> () -> Int {
- var runningTotal = 0
- func incrementer() -> Int {
- runningTotal += amount
- return runningTotal
- }
- return incrementer
- }
返回類型makeIncrementer
是。這意味著它返回一個函數,而不是一個簡單的值。它返回的函數沒有參數,每次調用時都返回一個值。要了解函數如何返回其他函數,請參見函數類型作為返回類型。()?->?Int
Int
該makeIncrementer(forIncrement:)
函數定義了一個名為的整數變量runningTotal
,用于存儲將返回的增量器的當前運行總數。此變量初始化為值0
。
該makeIncrementer(forIncrement:)
函數有一個Int
參數標簽為forIncrement
的參數,參數名稱為amount
。傳遞給此參數的參數值指定runningTotal
每次調用返回的增量函數時應遞增多少。該makeIncrementer
函數定義了一個名為的嵌套函數incrementer
,它執行實際的遞增。此功能只是增加了amount
對runningTotal
,并返回結果。
單獨考慮時,嵌套incrementer()
函數可能看起來不常見:
- func incrementer() -> Int {
- runningTotal += amount
- return runningTotal
- }
該incrementer()
函數沒有任何參數,但它在其函數體內引用runningTotal
和引用amount
。它通過捕獲做到這一點參考,以runningTotal
和amount
從周圍的功能和其自身的函數體中使用它們。通過參考捕捉保證runningTotal
和amount
不消失的時候調用makeIncrementer
結束,而且也保證了runningTotal
可用下一次incrementer
函數被調用。
注意
作為優化,如果該值未被閉包變異,并且在創建閉包后該值未發生變化,則Swift可以代之以捕獲并存儲值的副本。
Swift還處理在不再需要變量時處理變量所涉及的所有內存管理。
這是一個實際的例子makeIncrementer
:
- let incrementByTen = makeIncrementer(forIncrement: 10)
此示例設置一個常量incrementByTen
,該常量調用以引用每次調用時添加10
到其runningTotal
變量的增量函數。多次調用該函數會顯示此行為:
- incrementByTen()
- // returns a value of 10
- incrementByTen()
- // returns a value of 20
- incrementByTen()
- // returns a value of 30
如果您創建第二個增量器,它將擁有自己存儲的對新的單獨runningTotal
變量的引用:
- let incrementBySeven = makeIncrementer(forIncrement: 7)
- incrementBySeven()
- // returns a value of 7
incrementByTen
再次調用原始增量器()會繼續增加其自己的runningTotal
變量,并且不會影響由incrementBySeven
以下內容捕獲的變量:
- incrementByTen()
- // returns a value of 40
注意
如果為類實例的屬性分配閉包,并且閉包通過引用實例或其成員來捕獲該實例,則將在閉包和實例之間創建一個強引用循環。Swift使用捕獲列表來打破這些強大的參考周期。有關更多信息,請參閱閉包的強引用周期。
閉包是參考類型
在上面的例子中,incrementBySeven
并且incrementByTen
是常量,但這些常量引用的閉包仍然能夠增加runningTotal
它們捕獲的變量。這是因為函數和閉包是引用類型。
無論何時將函數或閉包賦值給常量或變量,實際上都是將該常量或變量設置為對函數或閉包的引用。在上面的例子中,它是閉包的選擇,它incrementByTen
?引用的是常量,而不是閉包本身的內容。
這也意味著如果為兩個不同的常量或變量分配閉包,那么這兩個常量或變量都引用相同的閉包。
- let alsoIncrementByTen = incrementByTen
- alsoIncrementByTen()
- // returns a value of 50
- incrementByTen()
- // returns a value of 60
上面的例子顯示調用alsoIncrementByTen
與調用相同incrementByTen
。因為它們都引用相同的閉包,它們都會遞增并返回相同的運行總計。
逃離閉包
閉包是說逃避當封蓋作為參數傳遞給函數,但在函數返回之后被調用的函數。當聲明一個以閉包作為其參數之一的函數時,可以@escaping
在參數的類型之前寫入,以指示允許閉包轉義。
閉包可以轉義的一種方法是存儲在函數外部定義的變量中。作為示例,許多啟動異步操作的函數將閉包參數作為完成處理程序。函數在啟動操作后返回,但是在操作完成之前不會調用閉包 - 閉包需要轉義,以便稍后調用。例如:
- 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)
- // Prints "200"
- completionHandlers.first?()
- print(instance.x)
- // Prints "100"
Autoclosures
一個autoclosure是自動創建來包裝被真實作為參數傳遞給函數的表達式的封閉件。它不接受任何參數,當它被調用時,它返回包含在其中的表達式的值。這種語法方便使您可以通過編寫普通表達式而不是顯式閉包來省略函數參數周圍的大括號。
這是常見的來電稱取autoclosures的功能,但它不是常見的實現那種功能。例如,該assert(condition:message:file:line:)
函數為其condition
和message
參數采用autoclosure?;?它condition
僅在調試參數進行評估,并建立其message
僅在參數評估condition
是false
。
autoclosure允許您延遲評估,因為在您調用閉包之前,內部代碼不會運行。延遲評估對于具有副作用或計算成本高昂的代碼非常有用,因為它可以讓您控制何時評估該代碼。下面的代碼顯示了關閉延遲評估的方式。
- 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
,但不帶任何參數,返回一個字符串-a功能。()?->?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
參數而不是一個閉包。參數自動轉換為閉包,因為customerProvider
參數的類型用@autoclosure
屬性標記。
- // customersInLine is ["Ewa", "Barry", "Daniella"]
- func serve(customer customerProvider: @autoclosure () -> String) {
- print("Now serving \(customerProvider())!")
- }
- serve(customer: customersInLine.remove(at: 0))
- // Prints "Now serving Ewa!"
注意
過度使用autoclosures會使您的代碼難以理解。上下文和函數名稱應該明確表示正在推遲評估。
如果您想要允許轉義的autoclosure,請使用@autoclosure
和@escaping
屬性。@escaping
上面的Escaping Closures中描述了該屬性。
- // 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!"
在上面的代碼中,函數將閉包附加到數組,而不是調用作為customerProvider
參數傳遞給它collectCustomerProviders(_:)
的閉包customerProviders
。數組聲明在函數范圍之外,這意味著在函數返回后可以執行數組中的閉包。因此,customerProvider
必須允許參數的值轉義函數的作用域。