第八节课:闭包(一)
1.什么是闭包?
闭包是一个捕获了上下文常量或者是变量的函数。
func test(){
print("test")
}
上面的函数是一个全局函数,也是一种特殊的闭包
,只不过当前的全局函数并不捕获值
。
2.下面看一个官方文档中的例子:
func makeIncrementer() -> () -> Int {
var runningTotal = 10
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
内嵌函数要使用到外部函数的变量值。
上面的incrementer内嵌函数,也是闭包
3.闭包表达式
{ (param) -> ReturnType in
//方法体
}
我们也可以把我们的闭包声明成一个可选类型
let closure: ((Int) -> Int)?
closure = {(age: Int) in
return age
}
还可以通过let关键字将闭包声明为一个常量(也就意味着一旦赋值之后就不能更改)
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
}
尾随闭包
当我们把闭包表达式作为函数的最后一个参数,如果当前的闭包表达式很长,我们可以通过尾随闭包的书写方式来提高代码的可读性。
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
return by(a, b, c)
}
test(10, 20, 30){(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
return (item1 + item2 < item3)
}
var array = [1, 2, 3]
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: <)
利用上下文推断参数和返回值类型
单表达式可以隐士返回,既省略return关键字
参数名称的简写(比如我们的$0)
尾随闭包表达式
捕获值
我们来看下面的例子
func makeIncrementer() -> () -> Int {
var runningTotal = 10
print("----")
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
let makeInc = makeIncrementer()
print(makeInc())
print(makeInc())
print(makeInc())
<--输出结果-->
11
12
13
从结果中可以看出,每次的结构都是在上次函数执行的基础上累加的,但是我们所知的runningTotal是一个临时变量,按理说每次进入函数都是10,这里为什么会每次累加呢? 主要原因:内嵌函数捕获了runningTotal,不再是单纯的一个变量了
print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())
<--输出结果-->
11
11
11
为什么这么写就是11了?
查看SIL文件
总结一下:
一个闭包能够从上下文捕获已经定义的常量和变量,即使这些定义的常量和变量的原作用域不存在,闭包仍然能够在其函数体内引用和修改这些值
当每次
修改捕获值
时,修改的是堆区中的value值
当每次
重新执行当前函数
时,都会重新创建内存空间
所以上面的案例中我们知道:
makeInc
是用于存储makeIncrementer
函数调用的全局变量,所以每次都需要依赖上一次的结果而直接调用函数时,相当于每次都新建一个堆内存,所以每次的结果都是不关联的,即每次结果都是一致的
闭包是引用类型
看到这里其实还是有点不清楚究竟是怎么回事,那么我们来分析下IR代码
首先,先熟悉下IR基本语法
数组
[<elementnumber>x<elementtype>]
//example
alloca[24 x i8],align 8 24个i8都是0
iN:多少位的整形
结构体
%swift.refcounted = type{%swift.type*,i64}
//example
%T = type{<type list>}//这种和C语言的结构体类似
指针类型
<type>*
//example
i64*//64位整形
getelementptr
指令
LLVM中我们获取数组和结构体的成员,通过getelementptr
,语法规则如下:
<result> = getelementptr <ty>,<ty*> <ptrval>{,[inrange] <ty> <idx>}*
<result> = getelementptr inbounds <ty>,<ty>* <ptrval>{,[inrange] <ty> <idx>}*
看一个例子
struct munger_struct{
int f1
int f2
};
void munge(struct munger_struct *P){
P[0].f1 = P[1].f1+P[2].f2;
}
munger_struct* array[3];
int main(int argc, const char * argv[]){
munge(array);
return 0;
}
这里记录一下,C文件查看IR的脚本
clang -S -emit-llvm ${SRCROOT}/07CTest/testC.c > ./testC.ll && open testC.ll
还有一种方式,通过终端命令行
clang -Os -S -fobjc-arc -emit-llvm main.c -o main.ll
但是我发现两种方式产生的代码并不一样,这个后续再分析一下终端命令,主要先以Xcode脚本分析为主
- 第一个索引:
%struct.munger_struct* %13, i32 0
等价于 第一个索引类型 + 第一个索引值 ==》 共同决定 第一个索引的偏移量 - 第二个索引:
i32 0
再结合图来理解一下
int main(int argc, const char * argv[]) {
int array[4] = {1, 2, 3, 4};
int a = array[0];
return 0;
}
其中int a = array[0];这句对应的LLVM代码应该是这样的:
/*
- [4 x i32]* array:数组首地址
- 第一个0:相对于数组自身的偏移,即偏移0字节 0 * 4字节
- 第二个0:相对于数组元素的偏移,即结构体第一个成员变量 0 * 4字节
*/
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i64 0
总结一下
第一个索引不会改变返回的指针的类型,即ptrval前面的*对应什么类型,返回的就是什么类型
第一个索引的偏移量是由第一个索引的值和第一个ty指定的基本类型共同确定的
后面的索引是在数组或者结构体内进行索引
每增加一个索引,就会使得该索引使用的基本类型和返回的指针类型去掉一层(例如 [4 x i32] 去掉一层是 i32)
IR分析
查看makeIncrementer
方法
- 首先通过
swift_allocObject
创建swift.refcounted
结构体 - 然后将
swift.refcounted
转换为<{ %swift.refcounted, [8 x i8] }>*结构体(即Box)
- 取出结构体中
index等于1的成员变量
,存储到[8 x i8]*连续的内存空间中
- 将内嵌函数的地址存储到i8即void地址中
-
最后返回一个结构体
这里的%swift.refcounted
大家看像什么,如果还不知道,我们来看一下swift_allocObject
其实不就是HeapObject
~
仿写
通过上述的分析,仿写其内部的结构体,然后构造一个函数的结构体,将makeInc的地址绑定到结构体中
struct HeapObject {
var type: UnsafeRawPointer
var refCount1: UInt32
var refCount2: UInt32
}
//函数返回值结构体
//BoxType 是一个泛型,最终是由传入的Box决定的
struct FunctionData<BoxType>{
//内嵌函数地址
var ptr: UnsafeRawPointer
var captureValue: UnsafePointer<BoxType>
}
//捕获值的结构体
struct Box<T> {
var refCounted: HeapObject
var value: T
}
//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
var f: () ->Int
}
//下面代码的打印结果是什么?
func makeIncrementer() -> () -> Int{
var runningTotal = 10
//内嵌函数,也是一个闭包
func incrementer() -> Int{
runningTotal += 1
return runningTotal
}
return incrementer
}
let makeInc = VoidIntFun(f: makeIncrementer())
let ptr = UnsafeMutablePointer<VoidIntFun>.allocate(capacity: 1)
//初始化的内存空间
ptr.initialize(to: makeInc)
//将ptr重新绑定内存
let ctx = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) {
$0.pointee
}
print(ctx.ptr)
print(ctx.captureValue.pointee)
<!--打印结果-->
0x00000001000058c0
Box<Int>(refCounted: 07Test.HeapObject(type: 0x0000000100008038, refCount1: 2, refCount2: 2), value: 10)
终端命令查找0x00000001000058c0
(其中0x00000001000058c0
是内嵌函数的地址)
结论:所以当我们var makeInc2 = makeIncrementer()
使用时,相当于给makeInc2
就是FunctionData
结构体,其中关联了内嵌函数地址,以及捕获变量的地址,所以才能在上一个的基础上进行累加
总结
1、捕获值原理:在堆上开辟内存空间,并将捕获的值放到这个内存空间里
2、修改捕获值时:实质是修改堆空间的值
3、闭包
是一个引用类型
(引用类型是地址传递),闭包的底层结构(是结构体:函数地址 + 捕获变量的地址 == 闭包
)
4、函数
也是一个引用类型
(本质是一个结构体,其中只保存了函数的地址),例如还是以makeIncrementer
函数为例
func makeIncrementer(inc: Int) -> Int{
var runningTotal = 1
return runningTotal + inc
}
var makeInc = makeIncrementer
分析其IR代码,函数在传递过程中,传递的就是函数的地址
5、每次重新执行当前函数,会重新创建新的内存空间