閉包
- 閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。
Swift
中的閉包與C
和Objective-C
中的代碼塊(blocks)以及其他一些編程語言中的匿名函數比較相似。 - 閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用,被稱為包裹常量和變量。
Swift
會為你管理在捕獲過程中涉及到的所有內存操作。 - 也可以說閉包是一個捕獲了上下文的常量和變量的函數。
- 閉包的表現形式:
- 1,全局函數是一個有名字但不會捕獲任何值的閉包
- 2,嵌套函數是一個有名字并可以捕獲其封閉函數域內值的閉包
- 3,閉包表達式是一個利用輕量級語法所寫的可以捕獲其上下文中變量或常量值的匿名閉包
閉包表達式
- 完整的閉包表達式要具備:
- 作用域{}
- 參數和返回值
- 函數體-(in)之后的代碼
{ (parameters) -> return type in
statements
}
-
Swift
中的閉包可以當做變量,也可以當做參數傳遞,也可以將它聲明為一個可選類型,還可以通過let
聲明為一個常量,也可以作為函數的參數使用
// 聲明為一個變量
var closure : (Int) -> Int = { (age: Int) in
return age
}
// 聲明為一個可選項
// 錯誤寫法
var closure : (Int) -> Int?
closure = nil
// 正確寫法
var closure : ((Int) -> Int)?
closure = nil
// 聲明為一個常量
let closure: (Int) -> Int
closure = {(age: Int) in
return age
}
// 閉包作為參數
func test(param : () -> Int){
print(param())
}
var age = 10
test { () -> Int in
age += 1
return age
}
- 閉包表達式是一種利用簡潔語法構建內聯閉包的方式。閉包表達式提供了一些語法優化,使得撰寫閉包變得簡單明了。下面閉包表達式的例子通過使用幾次迭代展示了
sorted(by:)
方法定義和語法優化的方式。每一次迭代都用更簡潔的方式描述了相同的功能。
var array = [4, 2, 3]
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1
< item2 })
??
array.sort{(item1 : Int, item2: Int) -> Bool in return item1 < item2 }
??
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
??
array.sort(by: {(item1, item2) in return item1 < item2 })
??
array.sort{(item1, item2) in item1 < item2 }
??
array.sort{ return $0 < $1 } //self
??
array.sort{ $0 < $1 }
??
array.sort(by: <)
根據上下文推斷類型
- 因為排序閉包函數是作為
sorted(by:)
方法的參數傳入的,Swift
可以推斷其參數和返回值的類型。sorted(by:)
方法被一個整型數組調用,因此其參數必須是(Int, Int) -> Bool
類型的函數。這意味著(Int, Int)
和Bool
類型并不需要作為閉包表達式定義的一部分。因為所有的類型都可以被正確推斷,返回箭頭(->)和圍繞在參數周圍的括號也可以被省略:
array.sort(by: { item1, item2 in return item1 < item2 } )
- 實際上,通過內聯閉包表達式構造的閉包作為參數傳遞給函數或方法時,總是能夠推斷出閉包的參數和返回值類型。這意味著閉包作為函數或者方法的參數時,你幾乎不需要利用完整格式構造內聯閉包。
- 盡管如此,你仍然可以明確寫出有著完整格式的閉包。如果完整格式的閉包能夠提高代碼的可讀性,官方也鼓勵采用完整格式的閉包。而在
sorted(by:)
方法這個例子里,顯然閉包的目的就是排序。由于這個閉包是為了處理整型數組的排序,因此讀者能夠推測出這個閉包是用于整型處理的。
單表達式閉包隱式返回
- 單行表達式閉包可以通過省略
return
關鍵字來隱式返回單行表達式的結果,如上版本的例子可以改寫為:
array.sort(by: { item1, item2 in item1 < item2 } )
參數名稱縮寫
-
Swift
自動為內聯閉包提供了參數名稱縮寫功能,你可以直接通過$0,$1,$2
來順序調用閉包的參數,以此類推。 - 如果你在閉包表達式中使用參數名稱縮寫,你可以在閉包定義中省略參數列表,并且對應參數名稱縮寫的類型會通過函數類型進行推斷。
in
關鍵字也同樣可以被省略,因為此時閉包表達式完全由閉包函數體構成:
array.sort(by: { $0 < $1 } )
運算符方法
- 實際上還有一種更簡短的方式來編寫上面例子中的閉包表達式。
array.sort(by: <)
尾隨閉包
- 如果你需要將一個很長的閉包表達式作為最后一個參數傳遞給函數,可以使用尾隨閉包來增強函數的可讀性。尾隨閉包是一個書寫在函數括號之后的閉包表達式,函數支持將其作為最后一個參數調用。在使用尾隨閉包時,你不用寫出它的參數標簽:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函數體部分
}
// 以下是不使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure(closure: {
// 閉包主體部分
})
// 以下是使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure() {
// 閉包主體部分
}
- 上文的排序方法可以簡寫
array.sort(){ $0 < $1 }
- 如果閉包表達式是函數或方法的唯一參數,則當你使用尾隨閉包時,你甚至可以把 () 省略掉:
array.sort{ $0 < $1 }
- 當閉包非常長以至于不能在一行中進行書寫時,尾隨閉包變得非常有用。舉例來說,
Swift
的Array
類型有一個map(_:)
方法,這個方法獲取一個閉包表達式作為其唯一參數。該閉包函數會為數組中的每一個元素調用一次,并返回該元素所映射的值。具體的映射方式和返回值類型由閉包來指定。 - 當提供給數組的閉包應用于每個數組元素后,
map(_:)
方法將返回一個新的數組,數組中包含了與原數組中的元素一一對應的映射后的值。
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]
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
}
print(strings)
// strings 常量被推斷為字符串類型數組,即 [String]
// 其值為 ["OneSix", "FiveEight", "FiveOneZero"]
值捕獲
- 閉包可以在其被定義的上下文中捕獲常量和變量。即使定義這些常量和變量的原作用域已經不存在,閉包仍然可以在閉包函數體內引用和修改這些值。
-
Swift
中,可以捕獲值的閉包的最簡單形式是嵌套函數,也就是定義在其他函數的函數體內的函數。嵌套函數可以捕獲其外部函數所有的參數以及定義的常量和變量。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 10
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
let closure = makeIncrementer(forIncrement: 10)
print(makeIncrementer(forIncrement: 10)())
print(makeIncrementer(forIncrement: 10)())
print(makeIncrementer(forIncrement: 10)())
print(closure())
print(closure())
print(closure())
// 打印結果
20
20
20
20
30
40
-
makeIncrementer
返回類型為() -> Int
。這意味著其返回的是一個函數,而非一個簡單類型的值。該函數在每次調用時不接受參數,只返回一個Int
類型的值。 -
incrementer()
函數并沒有任何參數,但是在函數體內訪問了runningTotal
和amount
變量。這是因為它從外圍函數捕獲了runningTotal
和amount
變量的引用。捕獲引用保證了runningTotal
和amount
變量在調用完makeIncrementer
后不會消失,并且保證了在下一次執行incrementer
函數時,runningTotal
依舊存在。
為了優化,如果一個值不會被閉包改變,或者在閉包創建后不會改變,Swift 可能會改為捕獲并保存一份對值的拷貝。
Swift 也會負責被捕獲變量的所有內存管理工作,包括釋放不再需要的變量。
- 我們通過sil看看發生了什么,通過
alloc_box
創建一個空間給變量runningTotal
,后面還有對它的內存管理,alloc_box
就是在堆區分配一塊內存空間存儲值,會調用swift_allocObject
image
閉包是引用類型
- 上面的例子中,
closure
是常量,但是這些常量指向的閉包仍然可以修改其捕獲的變量的值。這是因為函數和閉包都是引用類型。 - 無論你將函數或閉包賦值給一個常量還是變量,你實際上都是將常量或變量的值設置為對應函數或閉包的引用。上面的例子中,指向閉包的引用
closure
是一個常量,而并非閉包內容本身。 -
這也意味著如果你將閉包賦值給了兩個不同的常量或變量,兩個值都會指向同一個閉包:
image
通過IR來分析
IR的一些簡單語法
- 我們這里只介紹我們用得到的語法
- 數組
[<elementnumber> x <elementtype>] // example alloca [24 x i8], align 8 //24個i8都是0
- 結構體
%T = type {<type list>} %swift.refcountd = type {%swift.type*, i64 }
- 指針類型
<type> * i64* // 64位的整型
-
getelementptr
指令,LLVM中我們獲取數組和結構體的成員,通過getelementptr
,語法規則如下:
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}* <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
- 通過一個例子來理解
getelementptr
指令
struct munger_struct {
int f1;
int f2;
};
void munge(struct munger_struct *P) {
P[0].f1 = P[1].f1+P[2].f2;
}
struct munger_struct array[3];
-
cd
到main.c
目錄下,輸入命令clang -emit-llvm -S main.c -o main.ll
,打開main.ll
,%13
就是數組的首個元素,%14
就是取出結構體的第一個元素也就是P[0].f1
image
分析上文中的closure
- 將上文的代碼轉為
IR
代碼,可以看到makeIncrementer
函數返回了一個結構體,第一個元素為void *
,第二個元素為%swift.refcounted*
image -
%swift.refcounted*
的定義,它是一個結構體指針
image - 再來看下上面結構體的賦值,可以看到第一個元素里面存的就是內嵌函數的地址
%12 = insertvalue { i8*, %swift.refcounted* } { i8* bitcast (i64 (%swift.refcounted*)* @"$s4main15makeIncrementer12forIncrementSiycSi_tF11incrementerL_SiyFTA" to i8*), %swift.refcounted* undef }, %swift.refcounted* %8, 1
-
第二個參數的結構
image - 知道了它們的結構體之后,我們將它轉化為對應的結構體如下:
struct HeapObject{
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
//
struct FuntionData<T>{
var ptr: UnsafeRawPointer // 內嵌函數的地址
var captureValue: UnsafePointer<T> // 捕獲值的結構體
}
struct Box<T> {
var refCounted: HeapObject
var valueBox: UnsafeRawPointer
var value: T
}
// 由于編譯器不能識別FuntionData,所以我們將它綁定到一個具體的結構體上
struct VoidIntFun {
var f: () ->Int
}
-
驗證我們上面的結論是否正確。
image -
在終端中查找打印的地址,可以看到它就是我們的內嵌函數
image -
通過
lldb
查看內存,我們直接查看closure
的內存,可以看到并沒有打印出我們想象中的結果
image -
將返回值綁定在一個結構體上
image
函數也是一種引用類型
- 函數是一個獨立的代碼塊,用來執行特定的任務。同時我們函數也可以被當做參數被傳遞,也可以賦值變量,這里我們定義一個簡單的函數來看一下:
func makeFunc(param:Int) -> Int {
var runningTotal = 10
return runningTotal + param
}
var m = makeFunc
-
查看它的IR代碼,可以看到和閉包的結構類似,只是賦值的時候第二個值為空
image -
我們也可以通過定義結構體的方式打印它
image - 函數的本質也是一個結構體,不過這個結構體里只保存了函數的地址
逃逸閉包
- 當一個閉包作為參數傳到一個函數中,但是這個閉包在函數返回之后才被執行,我們稱該閉包從函數中逃逸。當你定義接受閉包作為參數的函數時,你可以在參數名之前標注
@escaping
,用來指明這個閉包是允許“逃逸”出這個函數的。 - 一種能使閉包“逃逸”出函數的方法是,將這個閉包保存在一個函數外部定義的變量中。舉個例子,很多啟動異步操作的函數接受一個閉包參數作為
completion handler
。這類函數會在異步操作開始之后立刻返回,但是閉包直到異步操作結束后才會被調用。在這種情況下,閉包需要“逃逸”出函數,因為閉包需要在函數返回之后被調用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
-
someFunctionWithEscapingClosure(_:)
函數接受一個閉包作為參數,該閉包被添加到一個函數外定義的數組中。如果你不將這個參數標記為@escaping
,就會得到一個編譯錯誤。 - 默認的閉包都是非逃逸的,函數的生命周期和閉包的生命周期是一樣的,函數結束之后閉包的生命周期也就結束了;逃逸閉包出現的情況一般是延遲調用閉包,或者將它作為屬性存儲。
自動閉包
- 自動閉包是一種自動創建的閉包,用于包裝傳遞給函數作為參數的表達式。這種閉包不接受任何參數,當它被調用的時候,會返回被包裝在其中的表達式的值。這種便利語法讓你能夠省略閉包的花括號,用一個普通的表達式來代替顯式的閉包。
- 我們經常會調用采用自動閉包的函數,但是很少去實現這樣的函數。舉個例子來說,
assert(condition:message:file:line:)
函數接受自動閉包作為它的condition
參數和message
參數;它的condition
參數僅會在debug
模式下被求值,它的message
參數僅當condition
參數為false
時被計算求值。 - 自動閉包讓你能夠延遲求值,因為直到你調用這個閉包,代碼段才會被執行。延遲求值對于那些有副作用
(Side Effect)
和高計算成本的代碼來說是很有益處的,因為它使得你能控制代碼的執行時機。下面的代碼展示了閉包如何延時求值。
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出 "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出 "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"
- 盡管在閉包的代碼中,
customersInLine
的第一個元素被移除了,不過在閉包被調用之前,這個元素是不會被移除的。如果這個閉包永遠不被調用,那么在閉包里面的表達式將永遠不會執行,那意味著列表中的元素永遠不會被移除。請注意,customerProvider
的類型不是String
,而是() -> String
,一個沒有參數且返回值為String
的函數。
過度使用 autoclosures 會讓你的代碼變得難以理解。上下文和函數名應該能夠清晰地表明求值是被延遲執行的。