本章的重點三個關鍵字: 函數, 閉包,閉包表達式
首先來看一個簡單的函數:??func函數名(參數列表)?->返回類型 { 函數體?}
func greet(person: String, day: String) -> String {
? ? return "Hello \(person), today is \(day)."
?}?
greet(person: "Bob", day: "Tuesday")
要理解 Swift 中的函數(Functions)和閉包(Closures),你需要切實弄明白三件事情,我們把這三件事按照重要程度進 行了大致排序:
1. 函數可以被賦值給變量,也可以作為另一個函數的輸入參數,或者另一個函數的返回值來使用。
2. 函數能夠捕獲存在于其局部作用域之外的變量。
3. 有兩種方法可以創建函數,一種是使用 func 關鍵字,另一種是 { }。在 Swift 中,后一種被稱為閉包表達式。
有時候,新接觸閉包的人會認為上面這三點的重要順序是反過來的,并且會忽略其中的某一點, 或把閉包和閉包表達式混為一談。盡管這些概念確實容易引起困惑,但這三點卻是鼎足而立, 互為補充的,如果你忽視其中任何一條,終究會在函數的應用上,狠狠地摔上一跤。?
1. 函數可以被賦值給變量,也能夠作為函數的輸入和輸出
讓我們從一個簡單的函數開始,它會打印一個整數:?
func printInt(i: Int) {
? ? print("You passed \(i).")
}
printInt(i: 1) //You passed 1.
將函數賦值給一個變量 :
let funVar = printInt
funVar(2) //You passed 2.
這里值得注意的是,我們不能在 funVar 調用時包含參數標簽,而在 printInt 的調用 (像是 printInt(i: 2)) 卻要求有參數標簽。Swift 只允許在函數聲明中包含標簽,這些標簽不是函數類 型的一部分。也就是說,現在你不能將參數標簽賦值給一個類型是函數的變量,不過這在未來 的 Swift 版本中可能會有改變。?
我們也可以試試 在funcVar 包含參數標簽:
funVar(i: 3)? //? Extraneous argument label 'i:' in call
我們也能夠寫出一個接受函數作為參數的函數:?
func useFunction(function: (Int) -> () ) {
? ? function(3)?
}
useFunction(function: printInt)
// You passed 3.?
useFunction(function: funVar) // You passed 3.?
為什么函數可以作為變量使用的這種能力如此關鍵呢?因為它讓你很容易寫出 “高階” 函數,高 階函數將函數作為參數的能力使得它們在很多方面都非常有用,我們已經在內建集合類型中看 到過它的威力了。?
函數也可以返回其他函數:
func returnFunc() -> (Int) -> String {
? ? func innerFunc(i: Int) -> String {
? ? ? ? return "you passed \(i)"
? ? }
? ? return innerFunc
}
returnFunc()(3)//you passed 3
let myFunc = returnFunc()
myFunc(3) // you passed 3
2. 函數可以捕獲存在于它們作用域之外的變量 當函數引用了在其作用域之外的變量時,這個變量就被捕獲了,它們將會繼續存在,而不是在 超過作用域后被摧毀。?
為了研究這一點,讓我們修改一下 returnFunc 函數。這次我們添加一個計數器,每次調用這個 函數時,計數器將會增加:?
func counterFunc() -> (Int) -> String {
?var counter = 0
func innerFunc(i: Int) -> String {?
counter += i // counter 被捕獲?
return "Running total: \(counter)”
?}?
return innerFunc?
}?
一般來說,因為 counter 是 counterFunc 的局部變量,它在 return 語句執行之后就應該離開作 用域并被摧毀。但因為 innerFunc 捕獲了它,所以 Swift 運行時將一直保證它的存在,直到捕 獲它的函數被銷毀為止。我們可以多次調用 innerFunc,并且看到 running total 的輸出在增加:?
let f = counterFunc()
?f(3)// Running total: 3
?f(4) // Running total: 7?
如果我們再次調用 counterFunc() 函數,將會生成并捕獲一個新的 counter 變量:?
let g = counterFunc()?
g(2) // Running total: 2
?g(2) // Running total: 4?
這并不影響我們的第一個函數,它擁有屬于自己的 counter:
?f(2)// Running total: 9?
你可以將這些函數以及它們所捕獲的變量想象為一個類的實例,這個類擁有一個單一的方法 (也就是這里的函數) 以及一些成員變量 (這里的被捕獲的變量)。?
在編程術語里,一個函數和它所捕獲的變量環境組合起來被稱為閉包。上面 f 和 g 都是閉包的 例子,因為它們捕獲并使用了一個在它們作用域之外聲明的非局部變量 counter。?
3. 函數可以使用{ } 來聲明為閉包表達式
在 Swift 中,定義函數的方法有兩種。下面這個簡單的函數將會把數字翻倍:?
一種是使用 func 關鍵字。
func doubler(i: Int) -> Int {
?return i*2?
}
[1, 2, 3, 4].map(doubler)
// [2, 4, 6, 8]?
另一種方法是使用閉包表達式 {}。像之前那樣將它傳給 map:
?let doublerAlt = { (i: Int) -> Int in return i*2 } ? ? // {(參數列表) -> 返回類型 in 函數體?}
[1, 2, 3, 4].map(doublerAlt) // [2, 4, 6, 8]?
Note:?使用閉包表達式來定義的函數可以被想成函數的字面量 (function literals)
{} 與 func 相比,{} 的區別:?閉包表達式是匿名的,它們沒 有被賦予一個名字
{} 有以下三種使用方法,
1.它們被創建時將其賦值給一個變量 (就像我們這 里對 doubler > 進行的賦值一樣)?
2. 或者是將它們傳遞給另一個函數或方法
3.你可以在定義一個表達式的同時,對它進行 調用。這個方法在定義那些初始化時代碼多于一行的屬性時會很有用 (我們將在下面 的延遲屬性部分看到一個例子)
使用閉包表達式 {} 聲明的 doubler,和之前使用 func 關鍵字聲明的函數,除了在參數標簽上的處 理上略有不同以外,其實是完全等價的。它們甚至存在于同一個 “命名空間” 中,這一點和有些 編程語言有所不同。?
{} 與 func 相比, ?優勢:?閉包表達式可以簡潔得多,特別 是在像是 map 這樣的將一個快速實現的函數傳遞給另一個函數時,這個特點更為明顯
我們將 doubler map 的例子用更短的形式進行了重寫:?
[1, 2, 3].map { $0 * 2 } // [2, 4, 6]
之所以看起來和原來很不同,是因為這里使用了
?Swift 中的一些可以讓代碼更加簡潔的特性:
1.如果你將閉包作為參數傳遞,并且你不再用這個閉包做其他事情的話,就沒有必要先將 它存儲到一個局部變量中。可以想象一下比如 5*i 這樣的數值表達式,你可以把它直接 傳遞給一個接受 Int 的函數,而不必先將它計算并存儲到變量里。
[1, 2, 3].map( { (i: Int) -> Int in return i * 2 } )
2.如果編譯器可以從上下文中推斷出類型的話,你就不需要指明它了。在我們的例子中, 從數組元素的類型可以推斷出傳遞給 map 的函數接受 Int 作為參數,從閉包內的乘法結 果的類型可以推斷出閉包返回的也是 Int。
?[1, 2, 3].map( { i?in return i * 2 } )
3.如果閉包表達式的主體部分只包括一個單一的表達式的話,它將自動返回這個表達式的 結果,你可以不寫 return。
[1, 2, 3].map( { i in i * 2 } )
4.Swift 會自動為函數的參數提供簡寫形式,$0 代表第一個參數,$1 代表第二個參數,以 此類推。
[1, 2, 3].map( { $0 * 2 } )?
5.如果函數的最后一個參數是閉包表達式的話,你可以將這個閉包表達式移到函數調用的 圓括號的外部。這樣的尾隨閉包語法(trailing closure syntax) 在多行的閉包表達式中 表現非常好,因為它看起來更接近于裝配了一個普通的函數定義,或者是像 if (expr) { } 這樣的執行塊的表達形式。
[1, 2, 3].map() { $0 * 2 }
6.最后,如果一個函數除了閉包表達式外沒有別的參數,那么調用的時候在方法名后面的 圓括號也可以一并省略。?
?[1,2,3].map{$0*2}?
總結: 使用 { }?創建函數 ?配合?Swift 的6個?簡潔的特性, 可以寫出精簡的函數表達。一旦你習慣了這樣的語法以及函數式編程風格的話,它們很快就會看 起來很自然,移除這些雜亂的表達,可以讓你對代碼實際做的事情看得更加清晰,?你一定會為 語言中有這樣的特性而心存感激。一旦你習慣了閱讀這樣的代碼,你一眼就能看出這段代碼做 了什么,而想在一個等效的 for 循環中做到這一點則要困難得多。
小白福利:
如果你還不夠熟悉使用?{ } +Swift 的6個?簡潔的特性。你在嘗試提供閉包表達式時遇到一些謎一樣的錯誤的話,該如何定位問題呢?
方法:
將 閉包表達式寫成上面例子中的第一種包括類型的完整形式,這有助于理清錯誤到底在哪兒。一旦完整版本可以編譯通過,你就可以按6個?簡潔的特性逐漸將類型移除,直到 編譯無法通過, 就知道錯誤在哪兒了。如果造成錯誤的是你的代碼的話,在這個過程中相信你已經修復好這些代碼了。
還有一些時候,Swift 會要求你用更明確的方式進行調用。
例子:你要得到一個隨機數數組,一種 快速的方法就是通過 Range.map 方法,并在 map 的函數中生成并返回隨機數。
(0..<3).map { _ in Int.random(in: 1..<100) } // [53, 63, 88]?
這里,無論如 何你都要為 map 的函數提供一個參數。或者明確使用 _ 告訴編譯器你承認這里有一個參數,但 并不關心它究竟是什么:
當你需要顯式地指定變量類型時,你不一定要在閉包表達式內部來設定。比如,讓我們來定義?一個 isEven,它不指定任何類型:
let isEven = { $0 % 2 == 0 }?
在上面,isEven 被推斷為 Int -> Bool。這和 let i = 1 被推斷為 Int 是一個道理,因為 Int 是整數 字面量的默認類型
?Integer?字面量協議
這是因為標準庫中的 IntegerLiteralType 有一個類型別名:?
protocol ExpressibleByIntegerLiteral {
associatedtype IntegerLiteralType
/// 用`value` 創建一個實例。
init(integerLiteral value: Self.IntegerLiteralType)?
}?
/// 一個沒有其余類型限制的整數字面量的默認類型。 typealias IntegerLiteralType = Int?
如果你想要定義你自己的類型別名,你可以重寫默認值來改變這一行為:?
typealias IntegerLiteralType = UInt32
let i = 1 // i 的類型為UInt32.?
顯然,這不是一個什么好主意。?
不過,如果你需要 isEven 是別的類型的話,
也可以在閉包表達式中為參數和返回值指定類型:?
let isEvenAlt = { (i: Int8) -> Bool in i % 2 == 0 }?
你也可以在閉包外部的上下文里提供這些信息:?
let isEvenAlt2: (Int8) -> Bool = { $0 % 2 == 0 }?
let isEvenAlt3 = { $0 % 2 == 0 } as (Int8) -> Bool?
因為閉包表達式最常見的使用情景就是在一些已經存在輸入或者輸出類型的上下文中,所以這?種寫法并不是經常需要,不過知道它還是會很有用。
?當然了,如果能定義一個對所有整數類型都適用的 isEven 的泛用版本的計算屬性會更好:?
extension BinaryInteger {
var isEven: Bool { return self % 2 == 0 }?
}?
或者,我們也可以選擇為所有的 Integer 類型定義一個全局函數:?
func isEven<T: BinaryInteger>(_ i: T) -> Bool { return?i%2==0 }?
要把這個全局函數賦值給變量的話,你需要先決定它的參數類型。變量不能持有泛型函數,它 只能持有一個類型具體化之后的版本:?
let int8isEven: (Int8) -> Bool = isEven?
最后要說明的是關于命名的問題。要清楚,那些使用 func 聲明的函數也可以是閉包,就和用 { } 聲明的是一樣的。記住,閉包指的是一個函數以及被它所捕獲的所有變量的組合。而使用 { } 來 創建的函數被稱為閉包表達式,人們常常會把這種語法簡單地叫做閉包。但是不要因此就認為 使用閉包表達式語法聲明的函數和其他方法聲明的函數有什么不同。它們都是一樣的,它們都 是函數,也都可以是閉包。