在 swift進階八:閉包 & Runtime & Any等類型 中,我們介紹了閉包
捕獲變量
特性根时。本節(jié)描姚,我們繼續(xù)了解閉包
:
- 什么是
閉包
-
閉包
的結(jié)構(gòu)
-
逃逸閉包
與非逃逸閉包
自動閉包
- 函數(shù)作形參
1. 什么是閉包
來自維基百科的解釋:
在計算機科學中,
閉包
(Closure)夹界,又稱詞法閉包
(Lexical Closure)或函數(shù)閉包
(function closures)馆里,是在支持頭等函數(shù)
的編程語言
中實現(xiàn)詞法綁定
的一種技術(shù)
。閉包
在實現(xiàn)上是一個結(jié)構(gòu)體
可柿,它存儲
了一個函數(shù)
(通常是其入口地址
)和一個關(guān)聯(lián)
的環(huán)境
(相當于一個符號查找表
)
1.1 全局函數(shù)
一種特殊
的閉包
-
無捕獲變量
的全局函數(shù)
鸠踪,也屬于閉包
func test(){
print(a)
}
1.2 內(nèi)嵌函數(shù)
也是閉包
,會捕獲外部變量
:
(incrementer
是內(nèi)嵌函數(shù)
复斥,也是一個閉包
营密,捕獲了上層函數(shù)
的runningTotal
)
func makeIncrementer() -> (()-> Int){
var runningTotal = 10
// 內(nèi)嵌函數(shù)(也是一個閉包,捕獲了runningTotal)
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
print(makeIncrementer()) // (()-> Int)匿名函數(shù)
print(makeIncrementer()()) // Int
打印值:
1.3 閉包表達式
閉包
是一個匿名函數(shù)
- 所有
代碼
都在花括號內(nèi)
參數(shù)
和返回類型
在in
關(guān)鍵字之前in
之后是主體內(nèi)容
:
{ (參數(shù))-> 返回類型 in
// do something
}
- swift的
閉包
可以當作let 常量
目锭、var 變量
评汰,也可以當作參數(shù)
傳遞。
// 常量
let closure1: (Int) -> Int
// 變量
var closure2 : (Int) -> Int = { (age: Int) in
return age
}
// 參數(shù)傳遞
func test(params: ()->Int) {
print(params())
}
var age = 10
// 執(zhí)行(尾隨閉包)
test { () -> Int in
age += 1
return age
}
-
swift閉包
支持可選類型
:在()外
使用痢虹?
聲明
// 可選值:在()外使用被去?聲明
var closure: ((Int) -> Int)?
1.4 尾隨閉包
當閉包表達式
作為函數(shù)
的最后一個參數(shù)
時,可通過尾隨閉包
的書寫方式
來提高
代碼的可讀性
:
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item: Int, _ item3: Int) -> Bool) -> Bool{
return by(a, b, c)
}
// 常規(guī)寫法
test(10, 20, 30, by: { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
return itme1 + itme2 < itme3
})
// 快捷寫法(小括號提到最后一個參數(shù)前)
test(10, 20, 30) { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
return itme1 + itme2 < itme3
}
// 最簡潔寫法 (入?yún)⒅苯邮褂?0 $1 $2代替奖唯,單行代碼可省略return)
test(10, 20, 30) { $0 + $1 < $2 }
可以看到惨缆,最簡潔
的寫法
看上去非常舒服
,語義
表達清晰
閉包表達式
是swift
的語法
丰捷。使用閉包表達式
可以更簡潔
的傳遞信息
坯墨。好處多多:
- 利用上下文
推斷參數(shù)
和返回類型
單表達式
可以隱式返回
,省略return
關(guān)鍵字參數(shù)名稱
可以直接使用簡寫(如$0
,$1
,元組的$0.0
)尾隨閉包
可以更簡潔的表達
var array = [1, 2, 3]
// 1. 常規(guī)寫法
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })
// 2. 省略類型 (根據(jù)上下文,可自動推斷參數(shù)類型)
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
// 3. 省略返回類型 (根據(jù)上下文病往,可自動推斷返回類型)
array.sort(by: {(item1, item2) in return item1 < item2 })
// 4. 省略return (單行表達式捣染,可省略return)
array.sort{(item1, item2) in item1 < item2 }
// 5. 參數(shù)簡寫 (使用$0 $1,按位置順序獲取參數(shù))
array.sort{ return $0 < $1 }
// 6. 省略return (單行表達式停巷,可省略return)
array.sort{ $0 < $1 }
// 7. 使用高階函數(shù)耍攘,傳遞排序規(guī)則
array.sort(by: <)
2. 閉包的結(jié)構(gòu)
- 我們使用
變量
記錄閉包
榕栏,發(fā)現(xiàn)內(nèi)部屬性
也被記錄了。
(下面runningTotal
被記錄少漆,多次打印
時臼膏,runningTotal
結(jié)果不一樣。
如果直接調(diào)用
閉包示损,會發(fā)現(xiàn)runningTotal
結(jié)果一樣)
func makeIncrementer() -> (()-> Int){
var runningTotal = 10
// 內(nèi)嵌函數(shù)(也是一個閉包渗磅,捕獲了runningTotal)
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
// 函數(shù)變量 (存儲格式是怎樣?)
var makeInc = makeIncrementer()
print(makeInc()) // 打印: 11
print(makeInc()) // 打印: 12
print(makeInc()) // 打印: 13
print(makeIncrementer()()) // 打印: 11
print(makeIncrementer()()) // 打印: 11
print(makeIncrementer()()) // 打印: 11
Q: 把
函數(shù)
賦值給變量
检访,變量存儲
的是函數(shù)地址
還是什么
始鱼?
- 在
SIL
中看不到makeInc
的結(jié)構(gòu):
- 我們再往后一層,直接查看
ir
代碼:
拓展
IR
與IR語法
:
Swift編譯流程
- 將
swift源碼
編譯為AST語法樹
swiftc -dump-ast HTPerson.swift > ast.swift
- 生成
SIL
源碼
swiftc -emit-sil HTPerson.swift > ./HTPerson.sil
- 生成
IR
中間代碼
swiftc -emit-ir HTPerson.swift > ir.swift
- 輸出
.o
機器文件
swiftc -emit-object HTPerson.swift
ir語法 ?? 官方文檔
- 數(shù)組
[<elementnumber> x <elementtype>] // [數(shù)組梳理 x 數(shù)組類型] // example alloca[24 x i8]脆贵,align8 // 24個i8都是0
- 結(jié)構(gòu)體
%swift.refcounted = type { %swift.type*, i64 } // { 指針類型医清,類型} // example %T = type {<type list>} // 和C語言結(jié)構(gòu)體類似
- 指針類型
<type> * // example i64* // 64位的整形
getelementptr
指針別名
可通過getelementptr
讀取數(shù)組
和結(jié)構(gòu)體
的成員:<result> = getelementptr <ty>, <ty>* <ptrval> {, [inrange] <ty> <id x>}* <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
getelementptr
的讀取案例
:
( 創(chuàng)建一個c
語言工程,main.c
中加入測試代碼
)// example struct munger_struct { int f1; int f2; }; void munge(struct munger_struct *p) { p[0].f1 = p[1].f1 + p[2].f2; // 假設(shè)P是有3個元素的數(shù)組卖氨。就可以直接通過下標讀取 } struct munger_struct array[3]; int main(int argc, const char * argv[]) { munge(array); //調(diào)用 return 0; }
- LLVM中会烙,
C語言
需要使用Clang
輸出IR
文件:
clang -S -fobjc-arc -emit-llvm main.c
- 熟悉了
IR語法
后,回到這個代碼:
func makeIncrementer() -> (()-> Int){
var runningTotal = 10
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
// 函數(shù)變量 (存儲格式是怎樣筒捺?)
var makeInc = makeIncrementer()
-
輸出
IR文件
柏腻,分析結(jié)構(gòu):
按照
IR
分析的代碼,我們自定義結(jié)構(gòu)
系吭,讀取函數(shù)指針地址
和內(nèi)部屬性值
:
由于
無法
直接讀取()-> Int
函數(shù)的地址
五嫂,所以利用結(jié)構(gòu)體
地址就是首元素地址
的特性,將函數(shù)
設(shè)為結(jié)構(gòu)體第一個屬性
肯尺。通過讀取結(jié)構(gòu)體
指針地址
沃缘,獲取到函數(shù)地址
struct FunctionData<T> {
var pointer: UnsafeRawPointer
var captureValue: UnsafePointer<T>
}
struct Refcounted {
var pointer: UnsafeRawPointer
var refCount: Int64
}
struct Type {
var type: Int64
}
struct Box<T> {
var refcounted: Refcounted
var value: T // 8字節(jié)類型,可由外部動態(tài)傳入
}
struct BoxMetadata<T> {
var refcounted: UnsafePointer<Refcounted>
var undefA: UnsafeRawPointer
var type: Type
var undefB: Int32
var undefC: UnsafeRawPointer
}
// 由于無法直接讀取`()-> Int`函數(shù)的地址则吟,所以利用結(jié)構(gòu)體地址就是首元素地址的特性槐臀。將函數(shù)設(shè)為結(jié)構(gòu)體第一個屬性
struct VoidIntFunc {
var f: ()->Int
}
func makeIncrementer() -> (()-> Int){
var runningTotal = 10
// 內(nèi)嵌函數(shù)(也是一個閉包,捕獲了runningTotal)
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
// 使用struct包裝的函數(shù)
var makeInc = VoidIntFunc(f: makeIncrementer())
// 讀取指針
let ptr = UnsafeMutablePointer<VoidIntFunc>.allocate(capacity: 1)
// 初始化
ptr.initialize(to: makeInc)
// 將指針綁定為FunctionData<Box<Int>>類型,返回指針
let context = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) { $0.pointee }
// 打印指針值 與 內(nèi)部屬性值
print(context.pointer) // 打印 0x00000001000054b0
print(context.captureValue.pointee.value) // 打印 10
驗證
打印的函數(shù)地址
是否是makeIncrementer
函數(shù):
打開終端逾滥,輸入命令:nm -p 編譯后的machO文件地址 | grep 函數(shù)地址
// 例如: nm -p /Users/asetku/Library/Developer/Xcode/DerivedData/Demo-bhpsxmnrzusvmeaotyclgmelcxpp/Build/Products/Debug/Demo | grep 00000001000054b0
- 可以看到峰档,該地址正是
函數(shù)
的地址
- 使用
xcrun swift-demangle XXXX
命令,還原
函數(shù)名
所以:
函數(shù)
事故引用類型
寨昙,被賦值的變量
記錄了函數(shù)地址
。
函數(shù)內(nèi)
的變量
掀亩,會alloc
開辟空間舔哪,調(diào)用前后,會retain
和release
管理引用計數(shù)
拓展: 如果
函數(shù)
內(nèi)部多個屬性
槽棍,結(jié)構(gòu)
是怎樣呢捉蚤?func makeIncrementer() -> (()-> Int){ var aa = 10 var bb = 20 // 內(nèi)嵌函數(shù)(也是一個閉包抬驴,捕獲了runningTotal) func incrementer() -> Int { aa += 6 bb += 9 return bb } return incrementer } var makeInc = makeIncrementer()
基礎(chǔ)結(jié)構(gòu)沒有變化
相比起
單變量
,多了一個臨時結(jié)構(gòu)
,把兩個變量
分別用指針
記錄:
struct FunctionData<T> { var pointer: UnsafeRawPointer var captureValue: UnsafePointer<T> } struct Refcounted { var pointer: UnsafeRawPointer var refCount: Int64 } struct Type { var type: Int64 } struct Box<T> { var refcounted: Refcounted var value: T // 8字節(jié)類型缆巧,可由外部動態(tài)傳入 } // 多了一個Box2結(jié)構(gòu)布持,每個變量都是`Box`結(jié)構(gòu)的對象 struct Box2<T> { var refcounted: Refcounted var value1: UnsafePointer<Box<T>> var value2: UnsafePointer<Box<T>> } struct BoxMetadata<T> { var refcounted: UnsafePointer<Refcounted> var undefA: UnsafeRawPointer var type: Type var undefB: Int32 var undefC: UnsafeRawPointer } // 由于無法直接讀取`()-> Int`函數(shù)的地址,所以利用結(jié)構(gòu)體地址就是首元素地址的特性陕悬。將函數(shù)設(shè)為結(jié)構(gòu)體第一個屬性 struct VoidIntFunc { var f: ()->Int } func makeIncrementer() -> (()-> Int){ var aa = 10 var bb = 20 // 內(nèi)嵌函數(shù)(也是一個閉包题暖,捕獲了runningTotal) func incrementer() -> Int { aa += 6 bb += 9 return bb } return incrementer } // 使用struct包裝的函數(shù) var makeInc = VoidIntFunc(f: makeIncrementer()) // 讀取指針 let ptr = UnsafeMutablePointer<VoidIntFunc>.allocate(capacity: 1) // 初始化 ptr.initialize(to: makeInc) // 將指針綁定為FunctionData<Box<Int>>類型,返回指針 let context = ptr.withMemoryRebound(to: FunctionData<Box2<Int>>.self, capacity: 1) { $0.pointee } // 打印指針值 與 兩個內(nèi)部屬性值 print(context.pointer) // 打印: 0x00000001000050c0 print(context.captureValue.pointee.value1.pointee.value) // 打幼匠:10 print(context.captureValue.pointee.value2.pointee.value) // 打与事薄: 20
- 在
MachO文件
中校驗函數(shù)地址
的值
,確定是makeIncrementer
函數(shù):
3. 逃逸閉包與非逃逸閉包
-
逃逸閉包:
當閉包
作為一個實際參數(shù)
傳遞給一個函數(shù)
,且在函數(shù)返回
之后調(diào)用
拼岳,我們就說這個閉包逃逸
了枝誊。
(逃逸閉包
作為函數(shù)形參
時,需要使用@escaping
聲明惜纸,生命周期
比函數(shù)
要長
叶撒。如:被外部變量持有
或異步延時調(diào)用
) -
非逃逸閉包:
系統(tǒng)默認
閉包參數(shù)是@nonescaping
聲明, 是非逃逸閉包
,生命周期
與被調(diào)用
的函數(shù)
保持一致
耐版。
3.1 逃逸閉包
-
閉包
被函數(shù)外變量
持有祠够,需要@escaping
聲明為逃逸閉包
-
閉包
被異步線程
延時調(diào)用,需要@escaping
聲明為逃逸閉包
class HTPerson {
var completion: ((Int)->Void)?
func test1(handler: @escaping (Int)->Void) {
// 1. 外部變量持有handler椭更,handler的生命周期逃到了`makeIncrementer`函數(shù)外
self.completion = handler
}
func test2(handler: @escaping (Int)->Void) {
// 2. 異步線程延時調(diào)用handler哪审,handler的生命周期逃到了`makeIncrementer`函數(shù)外
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
handler(10)
}
}
}
3.2 非逃逸閉包
系統(tǒng)默認
閉包為非逃逸閉包
,編譯期
自動加上@noescape
聲明,生命周期
與函數(shù)一致
虑瀑。
func test(closure: (()->())) {}
-
SIL編譯
可以看到默認使用@noescape
聲明閉包:
4. 自動閉包
-
自動閉包
:自動識別
閉包返回值
湿滓,可直接接收
返回值類型數(shù)據(jù)
。
需要用@autoclosure
聲明舌狗,不接收
任何參數(shù)
叽奥,返回值
是當前內(nèi)部表達式
的值
(如()->String
)
// 自動閉包`@autoclosure`聲明
func testPrint(_ message: @autoclosure ()->String) {
print(message())
}
func doSomeThing() -> String {
return "吃了嗎?"
}
// 入?yún)鱜函數(shù)`
testPrint(doSomeThing())
// 入?yún)鱜字符串`
testPrint("干啥呢")
- 可以看到痛侍,使用
自動閉包
時朝氓,參數(shù)可以是函數(shù)
,也可以是閉包返回類型
(字符串).
自動閉包
可以兼容函數(shù)入?yún)?/code>類型(
函數(shù)
/函數(shù)返參類型
)
5. 函數(shù)作形參
當
函數(shù)
作為參數(shù)
進行傳遞
時主届,可節(jié)省計算量
赵哲,在合適時期
再執(zhí)行
。普通函數(shù):
doSomeThing
是一個耗時操作
君丁,計算結(jié)果
傳給testPrint
時枫夺,testPrint
由于條件不滿足
,壓根沒用到
這個耗時操作
的結(jié)果
绘闷。
func testPrint(_ condition: Bool, _ message: String) {
if condition {
print("錯誤信息: \(message)")
}
print("結(jié)束")
}
func doSomeThing() -> String {
print("執(zhí)行了")
// 耗時操作橡庞,從0到1000拼接成字符串
return (0...1000).reduce("") { $0 + " \($1)"}
}
testPrint(false, doSomeThing())
- 使用函數(shù)作為入?yún)ⅲ?br>
入?yún)?/code>直接
傳入函數(shù)
较坛,未滿足條件
時,不
會執(zhí)行函數(shù)
扒最,避開了耗時操作
func testPrint(_ condition: Bool, _ message: ()->String) {
if condition {
print("錯誤信息: \(message())")
}
print("結(jié)束")
}
func doSomeThing() -> String {
print("執(zhí)行了")
// 耗時操作丑勤,從0到1000拼接成字符串
return (0...1000).reduce("") { $0 + " \($1)"}
}
testPrint(false, doSomeThing())
注意
- 上述只是演示
函數(shù)
作為入?yún)?/code>,
沒有
或單次
調(diào)用時吧趣,可延時
在合適時期調(diào)用
法竞,避免資源提前計算
。- 但如果該
資源
會被多次調(diào)用
再菊,還是提前計算資源
才節(jié)省資源
爪喘。
至此,我們對閉包
有了完整認識
纠拔。下一節(jié)介紹Optional
& Equatable
& 訪問控制