Swift-闭包(Closure) 上

279 阅读6分钟

1.闭包的定义

一个函数和它所捕获的变量常量环境组合起来,被称为闭包

  • 一般指定义在函数内部的函数

  • 一般捕获的是外层函数的局部变量、常量

    func makeClosure() -> (Int) -> Int {

        var one = 100
        
        func makeClosure(num number:Int) -> Int {
            one += number
            return one
        }
        return makeClosure  //返回的makeClosure和one形成了闭包
    

    }

可以把闭包想象成一个类的实例对象

  • 内存在堆空间
  • 捕获的局部变量、常量就是对象的成员(存储属性)
  • 组成闭包的函数就是类内部定义的方法

闭包是自包含的功能代码块,跟COC中的代码块(block)和其他一些语言中的匿名函数相似

  • 闭包可以作为函数的参数也可以作为函数的返回值
  • 可以向OC中一样用于回调和反向传值

2.闭包表达式

闭包表达式可以理解为闭包的表现形式

{(param) -> (returnType) in
        
        //函数体代码
        
}

  • 参数有多个是,多个参数用逗号隔开
  • 参数列表的小括号可以省略
  • 返回值类型也可以省略
  • 当没有返回值是in可以省略
  • in可以看做是一个分隔符,将函数体和前面的参数、返回值分隔开

2.1、闭包作为变量或常量

var closure: (_ a: Int, _ b: Int) -> Int = {(a, b) -> Int in
        return 10
    }
    let closure1: (_ a: Double) -> Double = { a -> Double in
        return 100.0
    }
var x = closure(10, 20)
var y = closure1(200.0)


需要注意的一点是:

//错误的写法
var closure : (Int) -> Int? //正确的写法
var closure : ((Int) -> Int)? closure = nil 

2.2、闭包作为函数的参数

func closureFounction(param:(_ a: Int, _ b: Int) -> Int) {
        print("闭包返回值:\(param(100, 200))") //输出 100
}

closureFounction(param: {(num1, num2) -> (Int) in
        print("闭包执行了")
        return 100})

}

2.3、闭包作为返回值

func closureFounction(_ a: Int, _ b: Int) -> (_ a: Int) -> (Int) {
        return closure2
}

3.尾随闭包

当我们把闭包表达式作为函数的最后一个参数,如果当前的闭包表达式很长,我们可以通过尾随闭包的书写方式来提高代码的可读性。

func closureFounction(_ a: Int, _ b: Int, _ by:(_ x:Int) -> Int) {
      var num = by(20)
}

closureFounction(20, 20){ (x) in
      return 30
}



var array = [1, 2, 3]
array.sort(by: {(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 }
array.sort{ $0 < $1 }
array.sort(by: <)

太过于简单的写法不推荐,因为不利于代码阅读以及以后的维护

4.闭包捕获值

func make() {
    var i = 1
    let closure: (_ a: Int) -> () = { (a) in
        i += a
        
        print("closure:\(i)")
    }
    print(i)
    closure(1)
    print("after1:\(i)")
    closure(1)
    print("after2:\(i)")
    closure(1)
    print("after3:\(i)")
    closure(1)
    print("after4:\(i)")
}
make()

//输出结果
1
closure:2
after1:2
closure:3
after2:3
closure:4
after3:4
closure:5
after4:5


从输出结果看,在闭包内部调用给 i + 1 可以改变外部 i 的值,这个调用过程是怎么样的过程呢,来分析一下:

4.1、SIL分析

通过命令把项目中的main.swift编译为main.sil并打开,定位到make()函数位置并找到对应的闭包的实现位置

所以我们可以得出结论,在闭包调用捕获外部局部变量的时候,是把值捕获到了堆区,在使用的时候直接访问拿到的值。

  • 闭包可以在方法调用的时候捕获上下文中的常量或变量。
  • 即使这个方法的作用域已经不在,仍然可以修改捕获到的变量

4.2、全局变量和局部变量捕获时的区别

闭包在使用全局变量的时候不会捕获到闭包内部,而在使用局部变量的时候会捕获到闭包的内部再使用

  • 使用全局变量

    var a = 10 //全局变量 let closure = { a += 10 } closure()

通过SIL分析:

通过以上截图可以看出来,在使用全局变量的时候是直接使用,默认是不捕获的

  • 使用局部变量

待完善.....

5.闭包的本质

在分析闭包的本质之前,先简单看一下IR的语法

  • 数组

    [ x ] //elementnumber数组中存放的数据的数量 //elementtype数组中存放的数据的类型 //例子 alloca [24 * i8], align 8 //24个int8类型都是0

  • 结构体

    /*

    • T:结构体名称

    • :列表,即结构体的成员列表*/%T = type {} //和C语言的结构体类似//例子 /*

    • swift.refcounted:结构体名称

    • %swift.type*:swift.type指针类型

    • i64:64位整型 - 8字节 / %swift.refcounted = type { %swift.type, i64}

  • 指针

    *

    //例子 //64位的整型 - 8字节 i64*

  • getelementptr指令

LLVM中我们获取数组和结构体的成员,通过 getelementptr ,语法规则如下:

<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <id x>}*

<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;
}


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指定的基本类型共同确定的
  • 后面的索引是在数组或者结构体内进行索引
  • 每增加一个索引,就会使得该索引使用基本类型和返回的指针类型去掉一层(例如 [4 x i32] 去掉一层是 i32

5.1、从IR的角度分析结构体的本质

把下面这段代码编译为IR格式

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

所以根据以上代码可以还原出以下结构

struct ClosureData<Box>{
    var ptr: UnsafeRawPointer
    var object: UnsafePointer<Box>
}

struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
    
}

struct Box<T>{
    var object: HeapObject
    var value: T
}

下面验证一下还原的数据结构

func makeIncrementer() -> () -> Int {
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
//{ i8*, %swift.refcounted* }
struct ClosureData<Box>{
    var ptr: UnsafeRawPointer
    var object: UnsafePointer<Box>
}
struct HeapObject {
    var matedata: UnsafeRawPointer
    var refcount1: Int32
    var refcount2: Int32
    
}
struct Box<T>{
    var object: HeapObject
    var value: T
}

struct ClosureStruct {//用结构体包裹一下闭包
    var closure :() -> Int
}
var f = ClosureStruct(closure: makeIncrementer())
let ptr = UnsafeMutablePointer<ClosureStruct>.allocate(capacity: 1)//创建ClosureStruct类型的指针并分配一块内存空间
ptr.initialize(to: f)//内存空间初始化f
let ctx = ptr.withMemoryRebound(to: ClosureData<Box<Int>>.self, capacity: 1){
    $0.pointee
}//内存重新绑定为 ClosureData<Box<Int>>
print(ctx.ptr)
print(ctx.object)

ptr.deinitialize(count: 1)
ptr.deallocate()//输出
0x0000000100002c20
0x0000000100779c10

打开终端验证

通过终端输出就能在mach-o文件中找到对应的内嵌函数incrementer,也就是说闭包就是这样的结构。