什麼是閉包?閉包表示式又是什麼?
在Swift中,可以通過func
定義一個函數,也可以通過閉包表示式定義一個函數。
{
(參數列) -> 返回值型別 in
函數體程式碼
}
定義一個普通的函數:
func sum(_ v1: Int, _ v2: Int) -> Int { v2 + v2 }
let result = sum(10, 20)
print(result)
// 輸出:30
定義閉包:
var sum = {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}
let result = sum(10, 20)
print(result)
// 輸出:30
定義函數:
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
要想使用exec
函數,則必須傳入兩個Int
型別的引數和一個返回Int
型別的函數,然後exec
內部執行了傳入的函數。
func sum(_ a: Int, _ b: Int) -> Int {
return a + b
}
exec(v1: 10, v2: 20, fn: sum)
// 輸出:30
按照以往的知識,我們需要定義一個函數,然後把函數傳給exec
就行了。其實我們也可以使用閉包表示式。
exec(v1: 10, v2: 20, fn: {
(v1: Int, v2: Int) -> Int in
return v1 + v2
})
// 輸出:30
上面的閉包表示式還可以簡寫:
exec(v1: 10, v2: 20, fn: {
v1, v2 in return v1 + v2
})
// 輸出:30
如果函數的返回值是一個單一表示式,可以省略return
。
exec(v1: 10, v2: 20, fn: {
v1, v2 in v1 + v2
})
// 輸出:30
如果閉包表示式不想寫引數,可以使用美元符$序號
代替,序號從0開始,代表引數位置。
exec(v1: 10, v2: 20, fn: { $0 + $1 })
// 輸出:30
簡單的閉包表示式還可以直接使用運運算元代替。
exec(v1: 10, v2: 20, fn: +)
// 輸出:30
如果將一個很長的閉包表示式作為函數的最後一個實參,使用尾隨閉包可以增強函數的可讀性。
尾隨閉包是一個被書寫在函數呼叫括號外面(後面)的閉包表示式。
以呼叫上面的exec
函數為例:
exec(v1: 10, v2: 20) {
$0 + $1
}
// 輸出:30
如果閉包表示式是函數的唯一實參,而且使用了尾隨閉包的語法,那就不需要在函數名後邊寫圓括號。
定義函數:
func exec(fn: (Int, Int) -> Int) {
print(fn(10, 20))
}
呼叫方式:
// 方式一:
exec(fn: { $0 + $1 })
// 方式二:
exec() { $0 + $1 }
// 方式三:
exec { $0 + $1 }
/*
輸出:
30
30
30
*/
閉包和閉包表示式嚴格意義上來講並不是同一個概念。
一個函數和它所捕獲的變數/常數環境組合起來,稱為閉包。
範例程式碼:
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int {
num += 1
return num
}
return plus
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
/*
輸出:
1
3
6
10
*/
為什麼var num = 0
作為區域性變數還能一直累加?不是應該在函數執行完成後就被釋放了麼?我們通過組合一探究竟。
通過分析可以看到,函數返回的是一個地址,也就是變數fn
裡面存放的是函數地址。
組合程式碼就變得複雜一點了,並且出現了swift_allocObject
關鍵字,也就意味著在堆空間申請了一塊記憶體,記憶體存放的是num
的值。每次呼叫fn
,存取的num
都是同一塊記憶體地址,所以才會出現區域性變數也能一直累加的效果。
swift_allocObject
存放的是num
:第一步:原始碼斷點:
第二步:檢視swift_allocObject
返回的地址:
第三步:檢視rax
地址存放的初始化值:
第四步:執行fn(1)
後:
第五步:執行fn(2)
後:
結論: 內部函數一旦捕獲了外部的區域性變數,要想持續使用這個變數,就需要延遲變數的生命週期,所以在堆空間分配一塊記憶體來存放區域性變數的值。
思考:為什麼可以存取同一塊記憶體空間?
var fn = getFn()
fn佔用16個位元組,前8個位元組存放返回的函數地址(plus的封裝
),後8個位元組存放堆空間(num
)的地址。如果var fn2 = getFn()
,fn1
和fn2
前8個位元組可能相同,不同的是後面的8個位元組。
可以把閉包想象成是一個類的範例物件。
class Closure {
var num = 0
func plus(_ i: Int) -> Int {
num += i
return num
}
}
var cs = Closure()
print(cs.plus(1))
print(cs.plus(2))
print(cs.plus(3))
print(cs.plus(4))
/*
輸出:
1
3
6
10
*/
範例程式碼:
// 如果第一個數大於0,返回第一個數,否則返回第二個數
func getFirst(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
getFirst(10, 20) // 10
getFirst(-2, 20) // 20
getFirst(0, -4) // -4
把上面的程式碼修改如下:
func getNumber() -> Int {
print("getNumber")
let a = 10
let b = 10
return a + b
}
let result1 = getFirst(10, getNumber())
print(result1)
/*
輸出:
getNumber
10
*/
let result2 = getFirst(-1, getNumber())
print(result2)
/*
輸出:
getNumber
20
*/
分析:不管第一個數是否大於0,都會執行第二個引數傳入的函數,這樣整體有點浪費(效能/空間)。我們可以嘗試把函數第二個入參型別修改為函數型別。
優化程式碼:
typealias VoidFunc = () -> Int
func getFirst(_ v1: Int, _ v2: VoidFunc) -> Int {
print("getFirst")
return v1 > 0 ? v1 : v2()
}
func getNumber() -> Int {
print("getNumber")
let a = 10
let b = 10
return a + b
}
getFirst(10, getNumber)
/*
輸出:
getFirst
*/
getFirst(-1, getNumber)
/*
輸出:
getFirst
getNumber
*/
結果:只有需要的時候才會執行對應的程式碼。
但是,如果這樣修改後,每次都需要傳入一個函數會有點麻煩。Swift提供了自動閉包功能,可以把普通變數自動包裹成閉包,這樣就能滿足上面程式碼的所有的功能了。
關鍵字: @autoclosure
用法:在函數前面加上@autoclosure
關鍵字即可。
自動閉包程式碼:
typealias VoidFunc = () -> Int
func getFirst(_ v1: Int, _ v2: @autoclosure VoidFunc) -> Int {
print("getFirst")
return v1 > 0 ? v1 : v2()
}
getFirst(10, 20) // 10
getFirst(-1, 10) // 10
自動閉包特點:
@autoclosure
會將普通引數(例如,20
)封裝成閉包{ 引數 }
(例如,{ 20 }
);@autoclosure
只支援() -> T
(無參有返回值)格式的引數;@autoclosure
並非只支援最後一個引數,和位置沒有任何關係;@autoclosure
、無@autoclosure
,構成函數過載;@autoclosure
的地方最好明確註釋清楚:這個值會被延遲執行(有可能不執行)。延伸: 空合併運運算元??
使用了@autoclosure
技術。
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
通過陣列的排序看下閉包表示式是如何使用的。
定義函數:
var arr = [20, 52, 19, 3, 80, 72]
在Swift中,Array
為開發者提供了sort()
排序函數,開發者可以直接使用。
arr.sort()
print(arr)
// 輸出:[3, 19, 20, 52, 72, 80]
sort()
是升序的,如果要降序呢?我們可以使用另外一個函數進行自定義排序。
Array
提供的函數:
func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
可以看到,該函數讓傳入一個閉包表示式。使用規則如下:
呼叫方式一(普通函數):
func compare(i1: Int, i2: Int) -> Bool {
return i1 > i2
}
arr.sort(by: compare)
print(arr)
// 輸出:[80, 72, 52, 20, 19, 3]
呼叫方式二(閉包表示式):
arr.sort(by: {
(i1: Int, i2: Int) -> Bool in
return i1 > i2
})
arr.sort(by: { i1, i2 in return i1 > i2 })
arr.sort(by: { i1, i2 in i1 > i2 })
arr.sort(by: { $0 > $1 })
arr.sort(by: >)
arr.sort() { $0 > $1 }
arr.sort { $0 > $1 }
// 輸出:[80, 72, 52, 20, 19, 3]