美文网首页
swift进阶十三:闭包

swift进阶十三:闭包

作者: markhetao | 来源:发表于2020-12-24 17:33 被阅读0次

    swift进阶 学习大纲

    swift进阶八:闭包 & Runtime & Any等类型 中,我们介绍了闭包 捕获变量特性。本节,我们继续了解闭包:

    1. 什么是闭包
    2. 闭包结构
    3. 逃逸闭包非逃逸闭包
    4. 自动闭包
    5. 函数作形参

    1. 什么是闭包

    来自维基百科的解释:

    • 在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数编程语言中实现词法绑定的一种技术

    • 闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联环境(相当于一个符号查找表

    1.1 全局函数

    一种特殊闭包

    • 无捕获变量全局函数,也属于闭包
    func test(){
         print(a)
    }
    

    1.2 内嵌函数

    也是闭包,会捕获外部变量
    incrementer内嵌函数,也是一个闭包,捕获了上层函数runningTotal

    func makeIncrementer() -> (()-> Int){
        var runningTotal = 10
        
        // 内嵌函数(也是一个闭包,捕获了runningTotal)
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        
        return incrementer
    }
    
    print(makeIncrementer())   // (()-> Int)匿名函数
    print(makeIncrementer()()) // Int
    

    打印值:

    image.png

    1.3 闭包表达式

    • 闭包是一个匿名函数
    • 所有代码都在花括号内
    • 参数返回类型in关键字之前
    • in之后是主体内容
    { (参数)-> 返回类型 in
        // do something
    }
    
    • swift的闭包可以当作let 常量var 变量,也可以当作参数传递。
    // 常量
    let closure1: (Int) -> Int
    
    // 变量
    var closure2 : (Int) -> Int = { (age: Int) in 
         return age
    }
    
    // 参数传递
    func test(params: ()->Int) {
        print(params())
    }
    
    var age = 10
    // 执行(尾随闭包)
    test { () -> Int in
        age += 1
        return age
    }
    
    • swift闭包支持可选类型:在()外使用声明
    // 可选值:在()外使用?声明
    var closure: ((Int) -> Int)?
    

    1.4 尾随闭包

    闭包表达式作为函数最后一个参数时,可通过尾随闭包书写方式提高代码的可读性

    func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item: Int, _ item3: Int) -> Bool) -> Bool{
        return by(a, b, c)
    }
    
    // 常规写法
    test(10, 20, 30, by: { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
        return itme1 + itme2 < itme3
    })
    
    // 快捷写法(小括号提到最后一个参数前)
    test(10, 20, 30) { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
        return itme1 + itme2 < itme3
    }
    
    // 最简洁写法 (入参直接使用$0 $1 $2代替,单行代码可省略return)
    test(10, 20, 30) { $0 + $1 < $2 }
    

    可以看到,最简洁写法看上去非常舒服语义表达清晰

    闭包表达式swift语法。使用闭包表达式可以更简洁传递信息。好处多多:

    • 利用上下文推断参数返回类型
    • 单表达式可以隐式返回省略return关键字
    • 参数名称可以直接使用简写(如$0,$1,元组的$0.0)
    • 尾随闭包可以更简洁的表达
    var array = [1, 2, 3]
    
    // 1. 常规写法
    array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })
    
    // 2. 省略类型 (根据上下文,可自动推断参数类型)
    array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })
    
    // 3. 省略返回类型 (根据上下文,可自动推断返回类型)
    array.sort(by: {(item1, item2) in return item1 < item2 })
    
    // 4. 省略return (单行表达式,可省略return)
    array.sort{(item1, item2) in item1 < item2 }
    
    // 5. 参数简写 (使用$0 $1,按位置顺序获取参数)
    array.sort{ return $0 < $1 }
    
    // 6. 省略return (单行表达式,可省略return)
    array.sort{ $0 < $1 }
    
    // 7. 使用高阶函数,传递排序规则
    array.sort(by: <)
    

    2. 闭包的结构

    • 我们使用变量记录闭包,发现内部属性也被记录了。
      (下面runningTotal被记录,多次打印时,runningTotal结果不一样。
      如果直接调用闭包,会发现runningTotal结果一样)
    func makeIncrementer() -> (()-> Int){
        var runningTotal = 10
        
        // 内嵌函数(也是一个闭包,捕获了runningTotal)
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        
        return incrementer
    }
    
    // 函数变量 (存储格式是怎样?)
    var makeInc = makeIncrementer()
    
    print(makeInc()) // 打印: 11
    print(makeInc()) // 打印: 12
    print(makeInc()) // 打印: 13
    
    print(makeIncrementer()()) // 打印: 11
    print(makeIncrementer()()) // 打印: 11
    print(makeIncrementer()()) // 打印: 11
    

    Q: 把函数赋值给变量,变量存储的是函数地址还是什么

    • SIL中看不到makeInc的结构:
    image.png
    • 我们再往后一层,直接查看ir代码:

    拓展IRIR语法:

    image.png
    Swift编译流程
    1. swift源码编译为AST语法树
      swiftc -dump-ast HTPerson.swift > ast.swift
    2. 生成SIL源码
      swiftc -emit-sil HTPerson.swift > ./HTPerson.sil
    3. 生成IR中间代码
      swiftc -emit-ir HTPerson.swift > ir.swift
    4. 输出.o机器文件
      swiftc -emit-object HTPerson.swift

    ir语法 👉 官方文档

    • 数组
    [<elementnumber> x <elementtype>]  // [数组梳理 x 数组类型]
    // example
    alloca[24 x i8],align8 // 24个i8都是0
    
    • 结构体
    %swift.refcounted = type { %swift.type*, i64 }   // { 指针类型,类型}
    // example
    %T = type {<type list>}  // 和C语言结构体类似 
    
    • 指针类型
    <type> *
    // example
    i64*    // 64位的整形
    
    • getelementptr指针别名
      可通过getelementptr读取数组结构体的成员:
    <result> = getelementptr <ty>, <ty>* <ptrval> {, [inrange] <ty> <id x>}*
    <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
    
    • getelementptr读取案例
      ( 创建一个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;  // 假设P是有3个元素的数组。就可以直接通过下标读取
    }
     
    struct munger_struct array[3];
    
    int main(int argc, const char * argv[]) {
       munge(array); //调用
       return 0;
    }
    
    • LLVM中,C语言需要使用Clang输出IR文件:
      clang -S -fobjc-arc -emit-llvm main.c
      image.png
    • 熟悉了IR语法后,回到这个代码:
    func makeIncrementer() -> (()-> Int){
        var runningTotal = 10
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        return incrementer
    }
    // 函数变量 (存储格式是怎样?)
    var makeInc = makeIncrementer()
    
    • 输出IR文件,分析结构:

      image.png
    • 按照IR分析的代码,我们自定义结构,读取函数指针地址内部属性值

    由于无法直接读取()-> Int函数的地址,所以利用结构体地址就是首元素地址的特性,将函数设为结构体第一个属性。通过读取结构体指针地址,获取到函数地址

    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字节类型,可由外部动态传入
    }
    
    struct BoxMetadata<T> {
        var refcounted: UnsafePointer<Refcounted>
        var undefA: UnsafeRawPointer
        var type: Type
        var undefB: Int32
        var undefC: UnsafeRawPointer
    }
    
    // 由于无法直接读取`()-> Int`函数的地址,所以利用结构体地址就是首元素地址的特性。将函数设为结构体第一个属性
    struct VoidIntFunc {
        var f: ()->Int
    }
    
    func makeIncrementer() -> (()-> Int){
        var runningTotal = 10
        // 内嵌函数(也是一个闭包,捕获了runningTotal)
        func incrementer() -> Int {
            runningTotal += 1
            return runningTotal
        }
        
        return incrementer
    }
    
    // 使用struct包装的函数
    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 }
    
    // 打印指针值 与 内部属性值
    print(context.pointer)   // 打印 0x00000001000054b0
    print(context.captureValue.pointee.value) // 打印  10
    
    image.png

    验证打印的函数地址是否是makeIncrementer函数:

    image.png
    打开终端,输入命令:nm -p 编译后的machO文件地址 | grep 函数地址
    // 例如:
    nm -p /Users/asetku/Library/Developer/Xcode/DerivedData/Demo-bhpsxmnrzusvmeaotyclgmelcxpp/Build/Products/Debug/Demo | grep 00000001000054b0
    
    • 可以看到,该地址正是函数地址
      image.png
    • 使用xcrun swift-demangle XXXX命令,还原函数名
      image.png

    所以:
    函数事故引用类型,被赋值的变量记录了函数地址
    函数内变量,会alloc开辟空间,调用前后,会retainrelease管理引用计数

    拓展: 如果函数内部多个属性结构是怎样呢?

    func makeIncrementer() -> (()-> Int){
       var aa = 10
       var bb = 20
       // 内嵌函数(也是一个闭包,捕获了runningTotal)
       func incrementer() -> Int {
           aa += 6
           bb += 9
           return bb
       }
       return incrementer
    }
    var makeInc = makeIncrementer()
    
    1. 基础结构没有变化


      image.png
    2. 相比起单变量,多了一个临时结构,把两个变量分别用指针记录:

      image.png
    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字节类型,可由外部动态传入
    }
    
    // 多了一个Box2结构,每个变量都是`Box`结构的对象
    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`函数的地址,所以利用结构体地址就是首元素地址的特性。将函数设为结构体第一个属性
    struct VoidIntFunc {
       var f: ()->Int
    }
    
    func makeIncrementer() -> (()-> Int){
      var aa = 10
      var bb = 20
      // 内嵌函数(也是一个闭包,捕获了runningTotal)
      func incrementer() -> Int {
          aa += 6
          bb += 9
          return bb
      }
      return incrementer
    }
    
    // 使用struct包装的函数
    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 }
    
    // 打印指针值 与 两个内部属性值
    print(context.pointer)   // 打印: 0x00000001000050c0
    print(context.captureValue.pointee.value1.pointee.value) // 打印:10
    print(context.captureValue.pointee.value2.pointee.value) // 打印: 20
    
    image.png
    • MachO文件中校验函数地址,确定是makeIncrementer函数:
      image.png

    3. 逃逸闭包与非逃逸闭包

    • 逃逸闭包
      闭包作为一个实际参数传递给一个函数,且在函数返回之后调用,我们就说这个闭包逃逸了。
      (逃逸闭包作为函数形参时,需要使用@escaping声明,生命周期函数。如:被外部变量持有异步延时调用)
    • 非逃逸闭包
      系统默认闭包参数是@nonescaping声明, 是非逃逸闭包生命周期被调用函数保持一致

    3.1 逃逸闭包

    • 闭包函数外变量持有,需要@escaping声明为逃逸闭包
    • 闭包异步线程延时调用,需要@escaping声明为逃逸闭包
    class HTPerson {
        
        var completion: ((Int)->Void)?
        
        func test1(handler: @escaping (Int)->Void) {
            
            // 1. 外部变量持有handler,handler的生命周期逃到了`makeIncrementer`函数外
            self.completion = handler
        }
        
        func test2(handler: @escaping (Int)->Void) {
            
            // 2. 异步线程延时调用handler,handler的生命周期逃到了`makeIncrementer`函数外
            DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
                handler(10)
            }
        }
        
    }
    

    3.2 非逃逸闭包

    系统默认闭包为非逃逸闭包 ,编译期自动加上@noescape声明,生命周期函数一致

    func test(closure: (()->())) {}
    
    • SIL编译可以看到默认使用@noescape声明闭包:
      image.png

    4. 自动闭包

    • 自动闭包自动识别闭包返回值,可直接接收返回值类型数据
      需要用@autoclosure声明,不接收任何参数返回值是当前内部表达式(如()->String)
    // 自动闭包`@autoclosure`声明
    func testPrint(_ message: @autoclosure ()->String) {
        print(message())
    }
    
    func doSomeThing() -> String {
        return "吃了吗?"
    }
    // 入参传`函数`
    testPrint(doSomeThing()) 
    // 入参传`字符串`
    testPrint("干啥呢")
    
    • 可以看到,使用自动闭包时,参数可以是函数,也可以是闭包返回类型(字符串).
      自动闭包可以兼容函数入参类型(函数/函数返参类型

    5. 函数作形参

    • 函数作为参数进行传递时,可节省计算量,在合适时期执行

    • 普通函数:
      doSomeThing是一个耗时操作计算结果传给testPrint时,testPrint由于条件不满足,压根没用到这个耗时操作结果

    func testPrint(_ condition: Bool, _ message: String) {
        if condition {
            print("错误信息: \(message)")
        }
        print("结束")
    }
    
    func doSomeThing() -> String {
        print("执行了")
        // 耗时操作,从0到1000拼接成字符串
        return (0...1000).reduce("") { $0 + " \($1)"}
    }
    
    testPrint(false, doSomeThing())
    
    image.png
    • 使用函数作为入参:
      入参直接传入函数未满足条件时,执行函数,避开了耗时操作
    func testPrint(_ condition: Bool, _ message: ()->String) {
        if condition {
            print("错误信息: \(message())")
        }
        print("结束")
    }
    
    func doSomeThing() -> String {
        print("执行了")
        // 耗时操作,从0到1000拼接成字符串
        return (0...1000).reduce("") { $0 + " \($1)"}
    }
    
    testPrint(false, doSomeThing())
    
    image.png

    注意

    • 上述只是演示函数作为入参没有单次调用时,可延时合适时期调用,避免资源提前计算
    • 但如果该资源会被多次调用,还是提前计算资源节省资源

    至此,我们对闭包有了完整认识。下一节介绍Optional & Equatable & 访问控制

    相关文章

      网友评论

          本文标题:swift进阶十三:闭包

          本文链接:https://www.haomeiwen.com/subject/gessnktx.html