美文网首页
Swift进阶(五)闭包与闭包表达式

Swift进阶(五)闭包与闭包表达式

作者: Jax_YD | 来源:发表于2021-01-06 17:52 被阅读0次

    一、闭包表达式(Closure Expression)

    • 在Swift中,可以通过func定义一个函数,也可以通过闭包表达式定义一个函数
    ///函数
    func sum(_ v1: Int, _ v2: Int) -> Int {v1 + v2}
    
    ///闭包表达式
    var fn = {
        (v1: Int, v2: Int) -> Int in
        return v1 + v2
    }
    fn(10,20)
    ///或者这样
    {
        (v1: Int, v2: Int) -> Int in
        return v1 + v2
    }(10,20)
    

    闭包表达式的格式

    {
        (参数列表) -> 返回值类型 in
        函数体代码
    }
    

    闭包表达式的简写

    func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
        print(fn(v1, v2))
    }
    
    ///1、正常写法
    exec(v1: 10, v2: 20) { (v1: Int, v2: Int) -> Int in
        return v1 + v2
    }
    
    ///2、省略参数类型
    exec(v1: 10, v2: 20) {
        v1, v2 in return v1 + v2
    }
    
    ///3、省略return
    exec(v1: 10, v2: 20) {
        v1, v2 in v1 + v2
    }
    
    ///4、用美元符表示
    exec(v1: 10, v2: 20) { $0 + $1 }
    
    ///5、最简单
    exec(v1: 10, v2: 20, fn: +)
    

    尾随闭包

    • 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
    • 尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式。
    func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
        print(fn(v1, v2))
    }
    exec(v1: 10, v2: 20) { $0 + $1 }
    
    • 如果闭包表达式函数唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号了。
    func exec(fn: (Int, Int) -> Int) {
        print(fn(v1, v2))
    }
    exec(fn: { $0 + $1 })
    exec() { $0 + $1 }
    exec { $0 + $1 }
    
    示例:数组的排序
    @inlinable public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
    

    根据函数定义,我们只需要传入一个函数即可(排序规则)

    /// 返回true: i1排在i2前面
    /// 返回false: i1排在i2后面
    func cmp(i1: Int, i2: Int) -> Bool {
    // 大的排在前面
        return i1 > i2
    }
    var nums = [20, 1, 60, 25, 95, 8, 5]
    nums.sort(by: cmp)
    

    当然,我们还可以使用闭包表达式来定义排序规则

    nums.sort(by: {
        (i1: Int, i2: Int) -> Bool in
        return i1 < i2
    })
    nums.sort(by: { i1, i2 in return i1 < i2 })
    nums.sort(by: { i1, i2 in i1 < i2 })
    nums.sort(by: { $0 < $1 })
    nums.sort(by: <)
    nums.sort() { $0 < $1 }
    nums.sort { $0 < $1 }
    

    忽略参数

    • 返回值与传入的参数没有关系的时候,我们可以忽略参数
    func exec(fn: (Int, Int) -> Int) {
        print(fn(10,20))
    }
    exec { _,_ in 30}
    

    二、闭包(Closure)

    什么是闭包?

    • 一个函数和它所捕获的变量\常量环境组合起来,称为闭包
      ① 一般指:定义在函数内部的函数
      ② 一般它捕获的是外层函数的 局部 变量\常量
      ③ 闭包是引用类型
    typealias Fn = (Int) -> Int
    func getFn() -> Fn {
        var num = 10
        func plus(_ i: Int) -> Int {
            num += i
            return num
        }
        return plus
    } /// 返回的plus和num形成了闭包
    var fn1 = getFn()
    print(fn1(1))
    print(fn1(2))
    /*输出结果*/
    1
    3
    

    问题一

    • 这里我们注意到一个问题,当getFn()执行完毕之后,局部变量num会被销毁,那么plus函数又是怎么捕获的呢?
      下面我们通过窥探汇编代码来看一下:我们将断点打在return plus位置
      image.png
    • 通过上图我们可以看到,在return plus之前,编译器申请了一段堆空间的内存,在这里我们可以初步猜想num就是被存在这一个新分配的堆空间里面
    • 下面我们继续断点调试(注意上图中num的初始值为0,下面我们将num的初始值设置为10,这样方便观察)
      image.png
    • 通过上图我们可以看到,局部变量num确实是被捕获之后,放到了新分配的堆空间地址里面。

    问题二

    • 局部变量num是在什么时候被捕获的呢?
      下面我们做一个简单的实验
    typealias Fn = (Int) -> Int
    func getFn() -> Fn {
        var num = 10
        func plus(_ i: Int) -> Int {
            num += i
            return num
        }
        num = 20
        return plus
    } /// 返回的plus和num形成了闭包
    
    var fn1 = getFn()
    print(fn1(1))
    /*输出结果*/
    21
    
    • 可以看到,num的捕获是在return plus之前捕获,所以在return plus之前,num值的改变,并不影响捕获。
    • 注意:如果num是全局变量,并不会发出捕获的行为,因为全局变量并不会随着函数的销毁而销毁。(函数调用完,内存会被回收,生命周期结束)

    总结:一个函数和它所捕获的变量\常量环境组合起来,称为闭包
    ① 一般指:定义在函数内部的函数
    ② 一般它捕获的是外层函数的 局部 变量\常量
    我们也可以把闭包想象成一个类的实例对象
    ① 内存在堆空间
    ② 捕获的局部变量\常量就是对象的成员(存储属性)
    ③ 组成的闭包的函数就是类内部定义的方法

    问题三

    • fn1里面存储存放的是什么?(猜想:fn1里面存放的是num的堆空间地址值 和 plus的地址值)(注意:print(MemoryLayout.size(ofValue: fn1))的输出结果为16)
    • %rax%rdx 常作为函数的返回值使用
    • lea指令 load effective address, 加载有效地址,可以将有效地址传送到指定的的寄存器。指令形式是从存储器读数据到寄存器, 效果是将存储器的有效地址写入到目的操作数, 简单说, 就是C语言中的”&”.

    下面我们通过断点调试来看一下:
    1、首先我们再return plus处打一个断点,窥探一下汇编代码。

    image.png
    image.png
    • 下面我们换个地方打断点


      image.png
      image1.png
    • 可以看到getFn()之后,有两次movq,其中q代表8个字节。跟上一个的断点调试比较。我们知道%rax里面存放的是plus%rdx里面存放的是num的堆地址。结合本次的断点,我们可以得出结论,fn1里面存放的就是plus(前8个字节)和num(后8个字节)的地址。

    • 注意:%rax里面存放的并不是plus的实际地址值,而是一个简洁的地址值。可以理解为在plus地址上面加了一层包装。我们可以同时在plus函数内部打上断点。先读取%rax,然后再进入plus函数内部来比较一下,我们可以发现\color{red}{两个地址值是不相同的}

      image.png
    • 通过上文我们知道fn1的前8个字节存放的是plus经过包装后的地址值,那么执行汇编指令的时候,就要从前8个字节中取出地址,在执行call指令。这种情况下,编译器第一时间并不知道plus的地址,因此汇编代码\color{red}{不会是}callq 0x100003d70这种有固定地址的。而是这种callq *%rax(寄存器里面存储的东西是变化的)。

    • 接下来我们找到callq *%rax,并跟进去,会发里面会有jmp指令,跳转到plus函数里面

      image.png
    image1.png image2.png
    • 这里补充一点关于寄存器的知识:
      ① rax、rdx 常作为函数返回值使用
      ② rdi、rsi、rdx、rcx、r8、r9 等寄存器常用于存放函数参数
      ③ rsp、rbp 用于栈操作
      ④ rip 作为指令指针,1、存储着CPU下一条要执行的指令地址,2、一旦CPU读取一条指令,rip会自动指向下一条指令(存储下一条指令的地址)

    问题四

    • numi是怎么传入plus函数里面的呢?
      我们继续通过汇编来查看一下
      image.png
    image1.png image2.png

    三、自动闭包(@autoclosure)

    我们首先来看一下@autoclosure的使用
    我们先定义一个函数,用来获取第一个正数

    func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
        return v1 > 0 ? v1 : v2
    }
    print(getFirstPositive(10, 30))
    print(getFirstPositive(-1, 2))
    /*输出结果*/
    10
    2
    

    这里我们可以将v2改成一个函数,如下:

    func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
        return v1 > 0 ? v1 : v2()
    }
    getFirstPositive(10) { 20 }
    
    • 当然,如果函数体很长的话,这样写没什么问题,但是如果像我们上面写的那样,函数体很短,则可读性和美观性都不足。
    • 这个时候我们就可以使用@autoclosure
    func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
        return v1 > 0 ? v1 : v2()
    }
    getFirstPositive(10, 20)
    
    • @autoclosure 会自动将20封装成闭包{ 20 }
    • @autoclosure 只支持() -> T 格式的参数
    • @autoclosure 并非只支持最后一个参数
    • 空合并运算符??使用了@autoclosure的技术(public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T)
    • @autoclosure、无@autoclosure,构成了函数重载
    func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
        print("无 @autoclosure")
        return v1 > 0 ? v1 : v2()
    }
    func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
        print("有 @autoclosure")
        return v1 > 0 ? v1 : v2()
    }
    print(getFirstPositive(10, 20))
    print(getFirstPositive(45, {30}))
    /*输出结果*/
    有 @autoclosure
    10
    无 @autoclosure
    45
    

    注意:为了避免与期望冲突,使用了@autoclosure的地方最好注明清楚:这个值会被延迟执行

    • 下面我们来看一下为什么要注意这一点:
    func fn() -> Int {
        print("延迟执行")
        return 20
    }
    
    func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
        print("有 @autoclosure")
        return v1 > 0 ? v1 : v2()
    }
    print(getFirstPositive(10, fn()))
    /*输出结果*/
    有 @autoclosure
    10
    

    我们会发现,因为v1大于0,所以fn()并没有执行。
    正常来讲fn()代表函数的执行,但是这里并没有执行,所以这一点要注意。

    四、逃逸闭包

    当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中\color{red}{逃逸}。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注@escaping,用来指明这个闭包是允许逃逸出这个函数的。

    func fn() -> Int {
        return 20
    }
    
    func getFirstPositive(_ v1: Int, _ v2: @escaping ()->Int) -> ()->Int {
        return v2
    }
    var fn1 = getFirstPositive(10, fn) 
    
    • 一种能使闭包逃逸出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束之后才会被调用。在这种情况下,闭包需要逃逸出函数,因为闭包需要在函数返回之后被调用。例如:
    var completionHandlers: [() -> Void] = []
    func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
        completionHandlers.append(completionHandler)
    }
    

    someFunctionWithEscapingClosure(_:)函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为@escaping,就会得到一个编译错误。
    将一个闭包标记为@escaping意味着你必须在闭包中显式的引用self。比如说,在下面的代码中,传递到someFunctionWithEscapingClosure(_:)中的闭包是一个逃逸闭包,这意味着它需要显式的引用self。相对的,传递到someFunctionWithNonescapingClosure(_:)中的闭包是一个非逃逸闭包,这意味着它可以隐式引用self

    var compltionHandlers: [() -> Void] = []
    func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
        compltionHandlers.append(completionHandler)
    }
    
    func someFunctionWithNonecapingClosure(closure: () -> Void) {
        closure()
    }
    
    class SomeClass {
        var x = 10
        func doSomething() {
            someFunctionWithEscapingClosure {
                self.x = 100
            }
            someFunctionWithNonecapingClosure {
                x = 200
            }
        }
    }
    
    let instance = SomeClass()
    instance.doSomething()
    print(instance.x)
    // 打印出“200”
    
    compltionHandlers.first?()
    print(instance.x)
    // 打印出“100”
    

    相关文章

      网友评论

          本文标题:Swift进阶(五)闭包与闭包表达式

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