闭包
闭包是一个捕获了上下文的常量或者是变量的函数
func makeIncrementer() -> () -> Int {
var runningTotal = 18
func incrementer() -> Int {
runningTotal += 1
return runningTotal
}
return incrementer
}
在这个例子中 incrementer 就是一个闭包,通过函数捕获了外部的变量,这种形式就构成了闭包的先决条件。
闭包表达式
{ (param) -> (returnType) in
//do something
}
由定义可以看出,闭包表达式由 作用域(也就是大括号)、参数和返回值、in 关键字、函数体(in)之后的代码构成
闭包既可以作为参数也可以作为变量来使用
作为参数
示例:
func test(param : () -> Int){
print(param())
}
作为变量
let closure: (Int) -> Int = {(age: Int) in
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, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
return (item1 + item2 < item3)
})
其中闭包表达式是 Swift 语法。使用闭包表达式能更简洁的传达信息。当然闭包表达式的好处 有很多:
- 利用上下文推断参数和返回值类型
- 单表达式可以隐士返回,既省略 return 关键字
- 参数名称的简写(比如我们的 $0)
- 尾随闭包表达式
捕获值
在OC 中,block里,我们也涉及到捕获值的操作,例如如下
- (void)testBlock{
NSInteger i = 1;
void(^block)(void) = ^{
NSLog(@"block %ld:", i);
};
i += 1;
NSLog(@"before block %ld:", i);
block();
NSLog(@"after block %ld:", i);
}
结果:
**2022-02-07 10:13:27.048265+0800 Block[5073:64097] before block 2:
**2022-02-07 10:13:27.048753+0800 Block[5073:64097] block 1:
**2022-02-07 10:13:27.048828+0800 Block[5073:64097] after block 2:
由此可以看出block捕获的值并未随着外面的 i += 1; 操作而变化。在 OC中若我们需要改变block里的捕获的值,我们知道应该 在定义 i 时需要用__block来修饰。
那么在Swift 中又是啥样子的呢? 例如:
func closuretest() {
var i = 10
let closure = {
print("clusure = \(i)")
}
i += 1
closure()
}
closuretest()
结果:
clusure = 11
Program ended with exit code: 0
由此可见,Swift的 闭包和 OC的 block是有区别的。
Swift闭包 本质上是将值捕获到堆区,使用的时候 再去堆区那这个地址读取,拿到真实的值。 通过 SIL 代码可以看到
然后 在执行 closure 时,获取的是 堆上的地址
IR代码分析
数组
[<elementnumber> x <elementtype>]
//example
alloca [24 x i8], align 8 24个i8都是0
alloca [4 x i32] === array
结构体
%swift.refcounted = type {%swift.type*, i64}
//example 表示形式
%T = type {<type list>} //这种和C语言的结构体类似
指针类型
<type> *
//example
i64* //64位的整形
getelementptr 指令
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
//example
getelementptr inbounds %TSa, %TSa* %0, i32 0, i32 0
// 这里我们看 LLVM 官网当中的一个例子:
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代码应该是这样的:
a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i32 0
/*
- [4 x i32]* array:数组首地址
- 第一个0:相对于数组自身的偏移,即偏移0字节 0 * 4字节
- 第二个0:相对于数组元素的偏移,即结构体第一个成员变量 0 * 4字节
*/
总结:
- 第一个索引不会改变返回的指针的类型,也就是说ptrval前面的*对应什么类型,返回就是什么类型。
- 第一个索引的偏移量的是由第一个索引的值和第一个ty指定的基本类型共同确定的。
- 后面的索引是在数组或者结构体内进行索引
- 每增加一个索引,就会使得该索引使用的基本类型和返回的指针的类型去掉一层。
闭包数据结构
///数据结构:当前闭包的执行地址 + 捕获变量堆空间的地址
struct ClosureData<Box> {
var ptr: UnsafeRawPointer
var object: UnsafePointer<Box>
}
///实例对象的内存地址
struct HeapObject {
var metadata: UnsafeRawPointer
//64 位的 count 值
var refcount1: Int32
var refcount2: Int32
}
///只捕获了一个变量 T
struct Box<T> {
var object: HeapObject
var value: T
}
struct NoMeanStruct {
var f: () -> ()
}
func closuretest() {
var i = 10
let closure = { () -> () in
print("clusure = \(i)")
}
i += 1
let f = NoMeanStruct(f: closure)
let ptr = UnsafeMutablePointer<NoMeanStruct>.allocate(capacity: 1)
ptr.initialize(to: f)
let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1) {
$0.pointee
}
print(ctx.ptr)
print(ctx.object)
print(ctx.object.pointee.value)
print("end")
}
closuretest()
通过打印我们已经找到了closure 里捕获的变量,同时,我们还可以通过
Mach-o文件来找对应的函数地址,来证实我们的猜想。
这里我们需要借助nm -p命令。
nm -p <mach-o path> | grep <函数地址(不带0x)>
通过终端输出 看到在mach-o文件中找到对应的函数closuretest(),也就验证了闭包的确是这样的结构。
defer
定义
defer {} 里的代码会在函数 return 之前执行,无论函数是从哪个分支 return 的,是有错误抛出,还是自然而然走到最后一行,都会执行。
如果多个 defer 语句出现在同一作用域中,它们执行的顺序和添加的顺序是相反的。
例如:
func deferTest() {
defer {
print("1 defer")
}
defer {
print("2 defer")
}
print("start function")
}
deferTest()
其打印的结果是
验证了
defer 的定义。