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