本文系閱讀閱讀原章節後總結概括得出。由於需要我進行一定的概括提煉,如有不當之處歡迎讀者斧正。如果你對內容有任何疑問,歡迎共同交流討論。
本章主要是介紹函式、閉包相關的基礎知識。如果你對此已有了解可以直接跳過
關於函式,主要有三個重要的概念:
- 函式也是一種型別,和
Int
、String
型別等類似,函式可以被賦值給變數,也可以作為引數傳入、傳出別的函式。 - 函式體內,不僅可以使用它的引數,還可以使用函式外部被截獲的變數
- 建立函式有兩種方法:用
func
關鍵字或閉包表示式{ }
接下來詳細解釋這三個概念:
1. 函式可以賦值給變數,也可以作為引數傳入、傳出別的函式
在Swift和很多現代語言中,函式也是“一等公民”。它可以被賦值給變數,也可以作為引數傳入、傳出別的函式。理解這一點很重要,這是函數語言程式設計的基礎,就像指標在C語言中那麼重要。我們通過一段程式碼來演示函式賦值給變數:
//定義了一個函式
func printInt(i: Int) {
print("you passed \(i)")
}
//函式賦值給變數,函式名後不用加括號
let funVar = printInt
//通過函式名呼叫函式,需要傳入引數
funVar(2) //輸出結果:“you passed 2”
複製程式碼
函式也可以作為別的函式的引數來用:
// 這個函式的引數是一個 (Int) -> ()型別 的函式
func useFunction(funcParam: (Int) -> ()) {
funcParam(3)
}
//把之前的函式傳入,作為引數。輸出結果相同,都是:“you passed 3”
useFunction(funVar)
useFunction(printInt)
複製程式碼
掌握了這一點,我們就可以寫出很多有用的高階函式,這在第三章——“集合”中有所體現。
2. 函式體內可以使用它截獲的,位於函式外部的變數
我們先看一個例子,把函式作為另外一個函式的返回值:
// 返回的函式型別是:(Int) -> String
func returnFunc() -> (Int) -> String {
func innerFunc(i: Int) -> String {
return "you passed \(i) to the returned func"
}
return innerFunc
}
let myFnc = returnFunc()
print(myFnc(4))
//輸出結果:“you passed 4 to the returned func”
複製程式碼
如果函式引用了函式體外部的變數,我們說這個變數被函式“截獲”了。變數在超出自己的作用域後不會被釋放,因為還有這個函式引用著它。我們把剛剛的函式修改一下,新增一個count
變數:
func returnFunc() -> (Int) -> () {
var count = 0
func innerFunc(i: Int) {
count += i
print("running total is now \(i)")
}
return innerFunc
}
複製程式碼
每次呼叫returnFunc
函式都會獲得一個不同的函式:
let f = returnFunc()
f(3) // 會輸出:“running total is now 3”
f(4) // 會輸出:“running total is now 7”
let g = returnFunc()
g(2) // 會輸出:“running total is now 2”
g(2) // 會輸出:“running total is now 4”
f(2) // 會輸出:“running total is now 9”
複製程式碼
你可以把截獲了變數的函式理解為一個類,這個類有一個方法(就是這個函式自身)和多個成員變數(被截獲的變數)
3. 閉包表示式的{ }語法
除了用func
關鍵字建立函式之外,還可以用{ }
語法建立函式。先看一個普通的函式:
func doubler(i: Int) -> Int { return i * 2 }
let a = [1, 2, 3, 4].map(doubler) //a = [2, 4, 6, 8]
複製程式碼
等價的寫法是:
let doubler = { (i: Int) -> Int in return i * 3 }
let a = [1, 2, 3, 4].map(doubler) //a = [3, 6, 9, 12]
複製程式碼
用閉包表示式定義的函式,可以理解為函式的“字面量”。就像1
、"hello"
分別是Int
、String
型別的字面量一樣。與用func
關鍵字不同的是,用閉包表示式定義的函式是匿名的。如果想使用它,可以把它賦值給某個變數。
這裡我寫了return i * 3
並非寫錯,而是表示新定義的doubler
會替換之前的doubler
,所以輸出結果中陣列的每個元素都是之前的三倍。這也說明了用func
關鍵字和閉包表示式建立的函式是完全等價的。
閉包表示式的作用主要是在於簡化定義函式的方式。比如之前的double
可以做如下簡化:
let a = [1, 2, 3, 4].map { $0 * 2 } //a = [2, 4, 6, 8]
複製程式碼
如果使用閉包表示式,根本沒有必要建立doubler
函式,就可以完成map
。接下來我們以原來複雜的程式碼為例,一步一步詳細描述簡化過程:
let doubler = { (i: Int) -> Int in return i * 2 }
let a = [1, 2, 3, 4].map(doubler) //a = [3, 6, 9, 12]
複製程式碼
- 函式作為引數傳入別的函式中時,沒有必要把這個函式先賦值給一個臨時變數,再把臨時變數傳入到別的函式中。我們可以直接把函式寫在別的函式中。簡化結果:
let a = [1, 2, 3, 4].map({ (i: Int) -> Int in return i * 2 })
複製程式碼
- 不必指定編譯器能根據上下文推匯出的型別。比如在這個例子中,陣列的元素是
Int
型別,所以傳入到map
方法中的函式的引數型別也是Int
,由於函式中進行了乘法運算,所以返回值型別必然也是Int
。簡化結果:
let a = [1, 2, 3, 4].map({ i in return i * 2 })
複製程式碼
- 如果閉包內只有一條語句,那麼顯然返回值就是這條語句的值,
return
關鍵字就可以省略。簡化結果:
let a = [1, 2, 3, 4].map({ i in i * 2 })
複製程式碼
- 對於傳入的引數,可以不指定具體的名字。預設就是
$0
表示第一個引數,$1
表示第二個引數……簡化結果:
let a = [1, 2, 3, 4].map({ $0 * 2 })
複製程式碼
- 如果作為引數的函式是別的函式的最後一個引數,它可以寫在函式的括號外面。這就是“尾閉包”語法。簡化結果:
let a = [1, 2, 3, 4].map() { $0 * 2 }
複製程式碼
- 因為
map
函式沒有別的引數,所以可以省略括號。簡化結果:
let a = [1, 2, 3, 4].map { $0 * 2 }
複製程式碼
這樣的寫法一開始可能不太熟悉,但是習慣之後就會發現它在不損害程式碼可讀性的前提下,極大的簡化了程式碼量。新手在簡化閉包表示式時,可能會遇到各種錯誤。這時候最好先寫出它的完整表示式,然後按照上述規則一步一步簡化,直到遇到錯誤為止。這時再好好考慮當時是哪裡犯了錯。
標註函式型別
Swift會自動推導閉包表示式的型別,但有時我們也需要顯式的標出。比如這個函式:
let isEven = { $0 % 2 == 0 }
複製程式碼
它會被自動推導為Int -> Bool
型別的。如果我們需要人為指定它的型別,可以這麼寫:
let isEven = { (i: Int8) -> Bool in i % 2 == 0 }
複製程式碼
不過,在閉包內標註型別不是一個好方法,更優雅的解決方案是在閉包外標註型別:
var isEven: Int8 -> Bool = { $0 % 2 == 0 }
// 或者
let isEven = { $0 % 2 == 0 } as Int8 -> Bool
複製程式碼
最好的,也是比較複雜的解決方案是定義一個範型的isEven
方法:
func isEven<T: IntegerType>(i: T) -> Bool {
return i % 2 == 0
}
let int8isEven: Int8 -> Bool = isEven
複製程式碼
總結
用{ }
閉包表示式和func
關鍵字建立的都是函式,它們是等價的。而閉包是指截獲了外部變數的函式。因此,嚴格意義上來說閉包和閉包表示式是兩個不同的概念。閉包表示式就是函式,而閉包是可以截獲外部變數的函式。