閉包可以從定義它們的上下文中捕獲和存儲對任何常量和變量的引用。 這被稱為關閉這些常量和變量。 Swift處理所有的捕獲的內存管理。
在函數中介紹的全局和嵌套函數實際上是閉包的特殊情況。 關閉采取以下三種形式之一:
- 全局函數是具有名稱并且不捕獲任何值的閉包。
- 嵌套函數是具有名稱的閉包,并且可以從其封閉函數捕獲值。
- 閉包表達式是以輕量級語法編寫的未命名閉包,可以從其周圍的上下文中捕獲值。
Swift的閉包表達式有一個干凈,清晰的風格。 這些優化包括:
- 從上下文中推斷參數和返回值類型
- 單表達式閉包的隱式返回
- 縮寫參數名稱
- 尾隨閉包語法
閉包表達式
閉包表達式是一種以簡明的聚焦語法編寫內聯閉包的方法。 Closure表達式提供了幾種語法優化,以縮短的形式寫入閉包,而不會失去清晰度或意圖。 下面的閉包表達式示例通過在多個迭代中精簡sort(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"]
然而,這是一個相當長的方式來寫一個本質上是單表達式函數(a> b)。 在這個例子中,最好使用閉包表達式語法寫入排序閉包。
閉包表達式語法
Closure表達式語法具有以下一般形式:
{ (parameters) -> return type in
statements
}
閉包表達式語法中的參數可以是輸入輸出參數,但它們不能具有默認值。 如果命名variadic參數,可以使用可變參數。 元組也可以用作參數類型和返回類型。
下面的示例顯示了來自早期的向后(: :)函數的閉包表達式版本:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
請注意,此內聯閉包的參數聲明和返回類型與后向(: :)函數的聲明相同。 在這兩種情況下,它被寫為(s1:String,s2:String) - > Bool。 但是,對于內聯閉包表達式,參數和返回類型寫在大括號內,而不是在大括號外。
閉包的主體的開始由in關鍵字引入。 此關鍵字表示閉包的參數和返回類型的定義已完成,閉包的主體即將開始。
因為閉包的主體是這么短,它甚至可以寫在一行:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
這說明對sort()方法的整體調用保持不變。 一對括號仍然包裝該方法的整個參數。 然而,這個參數現在是一個內聯閉包。
從上下文中推斷類型
因為排序閉包作為參數傳遞給方法,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值必須由閉包返回。 因為閉包的主體包含一個返回Bool值的單個表達式(s1> s2),所以沒有歧義,并且可以省略return關鍵字。
速記參數名稱
Swift自動為內聯閉包提供了速記參數名稱,可以用來通過名稱$ 0,$ 1,$ 2等引用閉包的參數的值。
如果在閉包表達式中使用這些速記參數名稱,則可以從其定義中省略閉包的參數列表,并且將從預期的函數類型推斷速記參數名稱的數量和類型。 也可以省略in關鍵字,因為閉包表達式完全由其主體組成:
reversedNames = names.sorted(by: { $0 > $1 } )
這里,$ 0和$ 1引用閉包的第一個和第二個String參數。
運算符方法
實際上有一個更短的方式來編寫上面的閉包表達式。 Swift的String類型定義了大于運算符(>)的字符串特定實現,作為一個具有兩個類型為String的參數的方法,并返回一個Bool類型的值。 這與sorted(by :)方法所需的方法類型完全匹配。 因此,您可以簡單地傳入greater-than運算符,Swift將推斷您要使用其字符串特定的實現:
reversedNames = names.sorted(by: >)
尾隨閉包
如果你需要將一個閉包表達式作為函數的最后一個參數傳遞給一個函數,而閉包表達式是long,那么將它作為一個尾隨閉包來寫是很有用的。 尾隨閉包寫在函數調用的括號后面,即使它仍然是函數的參數。 當使用尾隨閉包語法時,不要將閉包的參數標簽寫為函數調用的一部分。
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語法部分中的字符串排序閉包可以在排序(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表達式還指定String的返回類型,以指示將存儲在映射輸出數組中的類型。
閉包表達式在每次被調用時構建一個名為output的字符串。 它使用余數運算符(數字%10)計算數字的最后一位數字,并使用此數字在digitNames字典中查找合適的字符串。 閉包可以用于創建大于零的任何整數的字符串表示。
捕獲值
閉包可以從定義它的周圍環境捕獲常量和變量。 然后,閉包可以引用并修改那些常量和變量的值,即使定義常量和變量的原始作用域不再存在。
在Swift中,可以捕獲值的閉包的最簡單形式是嵌套函數,寫在另一個函數的主體內。 嵌套函數可以捕獲其任何外部函數的參數,也可以捕獲外部函數中定義的任何常量和變量。
這里有一個名為makeIncrementer的函數示例,它包含一個名為incrementer的嵌套函數。 嵌套的incrementer()函數從其周圍的上下文中捕獲兩個值,即runningTotal和amount。 捕獲這些值之后,incrementer由makeIncrementer返回,作為每次調用時將runningTotal增量的閉包。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
incrementer()函數沒有任何參數,但它從函數體內引用runningTotal和amount。 它通過從周圍函數捕獲對runningTotal和amount的引用,并在其自身的函數體中使用它們。 通過引用捕獲確保runningTotal和amount不會在調用makeIncrementer結束時消失,并且還確保runningTotal在下次調用incrementer函數時可用。
作為優化,如果該值未被閉包改變,并且該值在創建閉包后未被改變,Swift可能會捕獲并存儲值的副本。Swift還處理在不再需要時處理變量所涉及的所有內存管理。
這里有一個示例
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
閉包是引用類型
在上面的例子中,incrementByTen是常量,但這些常量引用的閉包仍然能夠增加他們捕獲的runningTotal變量。 這是因為函數和閉包是引用類型。
無論何時將函數或閉包分配給常量或變量,實際上都將該常量或變量設置為函數或閉包的引用。
在上面的示例中,它是incrementByTen引用的閉包的選擇,它是常量,而不是閉包本身的內容。這也意味著,如果你為兩個不同的常量或變量賦值一個閉包,那么這兩個常量或變量都將引用相同的閉包:
逃離閉包
當閉包作為參數傳遞給函數時,閉包被稱為逃逸函數,但在函數返回后被調用。 當你聲明一個函數將閉包作為其參數之一時,你可以在參數的類型之前寫入@escaping,以指示允許閉包逃逸。
閉包可以逃逸的一種方式是通過存儲在函數外部定義的變量中。 作為示例,許多啟動異步操作的函數將閉包參數作為完成處理程序。 該函數在啟動操作后返回,但是在操作完成之前不會調用閉包 - 閉包需要轉義,稍后調用。 例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_ :)函數接受一個閉包作為其參數,并將其添加到在函數外聲明的數組。 如果你沒有用@escaping標記這個函數的參數,你會得到一個編譯錯誤。
使用@escape標記閉包意味著你必須在閉包內自我引用。 例如,在下面的代碼中,傳遞給someFunctionWithEscapingClosure(_ :)的閉包是一個轉義閉包,這意味著它需要自我引用。 相反,傳遞給someFunctionWithNonescapingClosure(_ :)的閉包是一個非轉義閉包,這意味著它可以自我引用。
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"
自動閉包
自動閉包是一個自動創建的閉包,用于包裝作為參數傳遞給函數的表達式。它不接受任何參數,當它被調用時,它返回包裝在其中的表達式的值。這種語法方便的方法允許你通過寫一個正則表達式而不是一個顯式閉包來省略函數參數周圍的大括號。
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數組的第一個元素被closure中的代碼刪除,數組元素也不會被刪除,直到閉包被實際調用。 如果閉包從未被調用,則閉包中的表達式將永遠不會被計算,這意味著數組元素永遠不會被移除。 注意,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參數,而不是一個閉包。 參數將自動轉換為閉包,因為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屬性。
// 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數組。 數組被聲明在函數范圍之外,這意味著數組中的closures可以在函數返回之后執行。 因此,必須允許customerProvider參數的值轉義函數的作用域。