Swift-闭包上

149 阅读1分钟

闭包

闭包是一个捕获了上下文的常量或者是变量的函数

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 代码可以看到

image.png

然后 在执行 closure 时,获取的是 堆上的地址

image.png

IR代码分析

数组

  [<elementnumber> x <elementtype>] 
  //example 
  alloca [24 x i8], align 8      24i8都是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字节
*/

image.png

总结:

  • 第一个索引不会改变返回的指针的类型,也就是说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()

image.png 通过打印我们已经找到了closure 里捕获的变量,同时,我们还可以通过Mach-o文件来找对应的函数地址,来证实我们的猜想。

这里我们需要借助nm -p命令。

nm -p <mach-o path> | grep <函数地址(不带0x)>

image.png

通过终端输出 看到在mach-o文件中找到对应的函数closuretest(),也就验证了闭包的确是这样的结构。

defer

定义

defer {} 里的代码会在函数 return 之前执行,无论函数是从哪个分支 return 的,是有错误抛出,还是自然而然走到最后一行,都会执行。

如果多个 defer 语句出现在同一作用域中,它们执行的顺序和添加的顺序是相反的。

例如:

func deferTest() {
    defer {
        print("1 defer")
    }
    defer {
        print("2 defer")
    }
    print("start function")
}
deferTest()

其打印的结果是

image.png 验证了 defer 的定义。