美文网首页
从零学习Swift 05:闭包表达式和闭包

从零学习Swift 05:闭包表达式和闭包

作者: 小心韩国人 | 来源:发表于2020-04-21 18:25 被阅读0次
    一:闭包表达式的定义

    在 Swfit 中,可以通过func定义一个函数,也可以通过闭包表达式定义一个函数.闭包表达式的格式为:

    {
    (参数列表) -> 返回值类型 in
    方法体
    }
    

    比如说我们使用func定义一个add方法:

    
    func add(a: Int, b: Int) -> Int{
        a + b
    }
    
    print(add(a: 10, b: 20))
    
    

    用闭包表达式也可以定义:

    
    let fn = {
        (a: Int , b: Int) -> Int in
        a + b
    }
    
    print(fn(10,20))
    
    
    二:闭包表达式的简写

    定义一个函数如下:

    // 接受两个 Int 类型的参数 , 和一个函数类型的参数
    func exec(a: Int, b: Int, fn: (Int, Int) -> Int){
        print(fn(a,b))
    }
    

    使用闭包表达式完整调用如下:

    exec(a: 10, b: 20, fn: {
        (v1: Int, v2: Int) -> Int in
        return v1 + v2
    })
    

    另外还有4中简写方式:

    1. 省略参数类型 和 返回值类型
      因为闭包表达式已经明确定义了参数类型和返回值类型都是 Int 类型,所以在调用闭包表达式的时候,编译器能自动类型推断,知道具体的类型.所以类型可以省略
    exec(a: 10, b: 20) {
        v1, v2 in
        return v1 + v2
    }
    
    1. 省略 return
      因为函数体代码是一个单一表达式,所以我们可以省略 return
    exec(a: 10, b: 20) {
        v1, v2 in v1 + v2
    }
    
    1. 省略参数,$0 $1分别代表第一个,第二个参数
    exec(a: 10, b: 20) {
        $0 + $1
    }
    
    1. 直接 +
      如果直接写一个运算符,编译器也知道是拿两个参数直接参与运算
    exec(a: 10, b: 20, fn: +)
    
    三:尾随闭包

    如果一个函数的最后一个实参是一个闭包表达式,并且这个闭包表达式的函数体代码很长,为了增强函数的可读性,这个闭包表达式可以采用尾随闭包.

    尾随闭包:尾随闭包是一个书写在函数调用括号外面的闭包表达式.

    像上面的exec函数,它的最后一个参数是一个闭包表达式,那我们我们可以使用尾随闭包来增强可读性:(事实上,编译器自动敲出来的就是尾随闭包)

    func exec(a: Int, b: Int, fn: (Int, Int) -> Int){
        print(fn(a,b))
    }
    
    // 尾随闭包
    exec(a: 10, b: 20) { (v1, v2) -> Int in
        return v1 + v2
    }
    
    

    如果函数只有一个参数,而且这个参数是个闭包表达式,并且使用了尾随闭包的写法,那么在调用的时候可以省略小括号():

    func add(fn: (Int, Int) -> Int){
        print(fn(20,10))
    }
    
    //正常写法
    add(fn: {$0 - $1})
    
    //尾随闭包写法
    add(){$0 - $1}
    
    //省略小括号()
    add{$0 - $1}
    
    

    示例:
    Swift 中的Array有一个排序方法:

    func sort(by areInIncreasingOrder: (Self.Element, Self.Element) throws -> Bool)
    

    接收两个参数,返回一个布尔值.假设第一个参数是v1,第二个参数是v2,它的意思是:如果返回true,v1排在v2前面;如果返回false,v1排在v2后面.

    常规写法是传入一个函数:

    func arraySort(){
        //排序规则函数
        func comp(v1: Int, v2: Int) -> Bool{
            //从大到小排序
            v1 > v2
        }
        
        var array = [4,2,10,19,13,8,22]
        //传入 comp 函数
        array.sort(by: comp(v1:v2:))
        print(array)
    }
    

    也可以使用尾随闭包:

    //闭包表达式示例  数组排序
    func arraySort(){
        var array = [4,2,10,19,13,8,22]
        
        //闭包表达式常规写法
        array.sort(by: {
            (v1: Int, v2: Int) -> Bool in
            return v1 > v2
        })
        
        //因为是最后一个参数,可以采用尾随闭包写法
        array.sort(){
            v1,v2 in v1 > v2
        }
        
        //因为是最后一个参数,并且是唯一一个参数,可以省略小括号
        array.sort{
            v1,v2 in v1 > v2
        }
        
        //使用 $ 替代参数
        array.sort{
            $0 > $1
        }
        
        //直接使用 >
        array.sort(by: >)
        
        print(array)
    }
    
    
    四:闭包

    闭包和闭包表达式完全是两个概念:

    1. 闭包表达式: 闭包表达式是定义函数的一种方式;
    2. 闭包: 闭包是一个函数和它所捕获的变量/常量环境组合起来,称作闭包.
      一般指定义在函数内部的函数
      一般它捕获的是外部函数的局部变量/常量

    思考一下以下代码的打印结果是什么:

    typealias Fn = (Int) -> Int
    func getFn() -> Fn{
        
        var num = 0
        
        func sum(_ v1: Int) -> Int{
            num += v1
            return num
        }
        
        return sum
    }
    
    var fn = getFn()
    print(fn(1))
    print(fn(2))
    print(fn(3))
    print(fn(4))
    

    结果如下:


    怎么会这样呢?num是在getFn函数内部定义的局部变量,在 149 行调用完getFn()后,num的内存应该被回收了,为什么打印的结果都是在同一块内存上累加的呢?

    其实这就是闭包,sum 函数捕获了 num 变量,并且把 num 变量存在了堆空间,所以即使函数调用完毕, num 变量依然没有销毁.

    我们来看一下它的汇编代码:

    向堆空间申请内存

    然后我们再修改一下sum函数的代码,使其不访问外部变量,再看看它的汇编,对比一下:

    sum 函数不访问外部变量

    汇编如下:

    可以看到,当sum函数没有访问外部变量的时候,它的汇编代码很简单.也没有调用allocObject向堆空间申请内存.并且getFn返回的直接就是sum函数的地址.

    现在,我们大概也知道了,当 sum 函数访问了外层函数的局部变量 num 时,会调用 allocObject 函数向堆空间申请内存,把 num 变量存储在堆空间,保住 num 变量的命.也就是所谓的 捕获.

    下面我们将通过汇编代码验证我们的结论.

    如果所示,打两个断点:

    断点

    运行代码:


    getFn 函数返回值

    上面没有访问外部变量时,getFn直接返回的就是sum函数的地址.但是这里不一样了,因为访问了外部变量,getFn返回的是一个堆空间的地址.这个地址中有可以找到sum函数地址的线索,并且还要存储捕获的num变量.

    继续跳到下一个断点:

    执行fn(1)

    执行完fn(1),内存中的值变成了 1.

    继续跳过断点,会执行fn(2):

    执行fn(2

    执行fn(3):

    执行fn(3)

    执行fn(4):

    执行fn(4)

    通过上面的分析,已经很清晰的知道了当函数访问了外部函数的局部变量后,会向堆空间申请内存,用来捕获外部函数的变量.

    思考一下,调用两次getFn()下面代码打印结果是什么?

    调用两次 getFn()

    我们看看它的汇编代码:

    调用两次getFn()

    返回了两个Fn对象:

    执行 fn1(1):

    执行 fn1(1)

    执行 fn2(2):

    执行 fn2(2)

    执行 fn1(3):

    执行 fn1(3)

    执行 fn2(4):

    执行 fn2(4

    可以看到,执行多少次getFn(),就会创建多少个Fn对象,并且每个对象之间互不影响.他们都有自己的内存空间,都有自己的捕获对象.

    上面的fn1 , fn2和它捕获的变量组合在一起就是一个闭包,其实闭包和类的实例对象很相似,我们通过两张图对比一下:

    闭包 和 实例对象对比

    另外他们都存储在堆空间,并且内存布局也一样,所以我们可以把闭包想象成一个实例对象,这样更容易理解.

    相关文章

      网友评论

          本文标题:从零学习Swift 05:闭包表达式和闭包

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