閉包
閉包是自包含的功能塊,可以在代碼中傳遞和使用。Swift中的閉包與C和Objective-C中的塊以及其他編程語言中的lambda類似。
閉包可以從定義的上下文中捕獲和存儲對任何常量和變量的引用。這被稱為關閉那些常量和變量。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表達式語法具有以下一般形式:
<pre class="code-voice" style="font-size: 0.85rem;">{(參數) - > 返回類型 in</pre>
<pre class="code-voice" style="font-size: 0.85rem;"> 聲明</pre>
<pre class="code-voice" style="font-size: 0.85rem;">}</pre>
該參數在封閉表達式語法可以在輸出參數,但是他們不能有一個默認值。如果命名可變參數,則可以使用變量參數。元組也可以用作參數類型和返回類型。
下面的例子顯示了backward(_:_:)
上面函數的閉包表達式版本:
<wiz_code_mirror><pre class=" CodeMirror-line " role="presentation">reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in</pre>
<pre class=" CodeMirror-line " role="presentation"> return s1 > s2</pre>
<pre class=" CodeMirror-line " role="presentation">})</pre>
<pre class=" CodeMirror-line " role="presentation">?</pre></wiz_code_mirror>
請注意,此內聯關閉的參數聲明和返回類型與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
,并返回一個類型值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
用閉包number
參數的值進行初始化,以便可以在閉包體中修改該值。(函數和閉包的參數始終是常量。)閉包表達式還指定返回類型String
,以指示將存儲在映射的輸出數組中的類型。
閉包表達式會在output
每次調用時創建一個字符串。它number
使用余數運算符(number % 10
)計算最后一位數字,并使用此數字在digitNames
字典中查找適當的字符串。閉包可以用來創建任何大于零的整數的字符串表示。
注意
對digitNames
字典下標的調用后面跟著一個感嘆號(!
),因為字典下標返回一個可選值,表示如果該鍵不存在,字典查找可能會失敗。在上面的例子中,保證字典number % 10
總是一個有效的下標鍵digitNames
,因此使用感嘆號來強制解開String
存儲在下標可選返回值中的值。
從檢索到的字符串digitNames
辭典被添加到前面的output
,有效地建立反向一數目的字符串版本。(該表達式number % 10
給出了6
for 16
,8
for 58
和0
for的值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
逃逸關閉
閉包是說逃避當封蓋作為參數傳遞給函數,但在函數返回之后被調用的函數。當你聲明一個將閉包作為其參數的函數時,你可以@escaping
在參數的類型之前寫入,以表明允許閉包。
閉包可以逃脫的一種方式是存儲在函數外部定義的變量中。作為例子,許多啟動異步操作的函數都將閉包參數作為完成處理程序。該函數在開始操作后返回,但在操作完成之前不會調用閉包 - 閉包需要轉義,稍后調用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
該someFunctionWithEscapingClosure(_:)
函數將閉包作為參數,并將其添加到在函數外聲明的數組。如果你沒有標記這個函數的參數@escaping
,你會得到一個編譯時錯誤。
標記閉包@escaping
意味著你必須self
在閉包中明確提及。例如,在下面的代碼中,傳遞給的閉包someFunctionWithEscapingClosure(_:)
是一個轉義閉包,這意味著它需要self
明確引用。相比之下,傳遞給someFunctionWithNonescapingClosure(_:)
它的閉包是一個nonescaping閉包,這意味著它可以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是自動創建來包裝被真實作為參數傳遞給函數的表達式的封閉件。它不需要任何參數,當它被調用時,它會返回包裝在其中的表達式的值。這種語法上的便利可以讓你通過寫一個普通的表達式而不是顯式的閉包來省略函數參數的大括號。
通常調用采用自動屏蔽的函數,但實現這種功能并不常見。例如,該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
,但() -> String
不帶任何參數,返回一個字符串-a功能。
當您將閉包作為參數傳遞給函數時,您會得到延遲評估的相同行為。
// 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
屬性標記其參數類型來采用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!"
注意
過度使用自動遮擋會使您的代碼難以理解。上下文和函數名稱應該明確表示評估正在推遲。
如果您想要允許轉義的自動關閉,請使用@autoclosure
和@escaping
屬性。該@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!"
在上面的代碼,而不是調用傳遞給它作為它的閉合customerProvider
參數,該collectCustomerProviders(_:)
函數將所述封閉的customerProviders
陣列。數組聲明在函數范圍之外,這意味著數組中的閉包可以在函數返回后執行。結果,customerProvider
參數的值必須被允許轉義該函數的作用域。