美文网首页
Swift 中的闭包

Swift 中的闭包

作者: CodingIran | 来源:发表于2019-08-29 18:03 被阅读0次

    闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 CObjective-C 中的代码块 (blocks) 以及其他一些编程语言中的匿名函数比较相似。
    闭包可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。

    闭包表达式

    Swift 中可以通过 func 关键字 定义一个函数

    func sum(_ v1: Int, _ v2: Int) -> Int {
        return v1 + v2
    }
    

    sum 函数调用:

    sum(10, 20)  // 30
    

    也可以用过闭包表达式定义一个函数

    var fn = { (v1: Int, v2: Int) -> Int in
        return v1 + v2
    }
    

    闭包表达式调用:

    fn(10, 20) // 30
    

    Swift 中函数是一类公民,可以作为参数返回值,跟 IntArrayClass 等没有区别:

    func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
        print(fn(v1, v2))
    }
    

    观察 exec 函数,这是有三个参数的无返回值函数:

    • 第1个参数:v1,类型为 Int
    • 第2个参数:v2,类型为 Int
    • 第1个参数:fn,类型为 (Int, Int) -> Int)
    • 无返回值

    调用 exec 函数:

    exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
        // 注意:这里的 v1 和 v2 是 fn 闭包表达式内的参数,跟 exec 函数的 v1、v2 没有任何关系
        return v1 + v2
    })
    

    闭包的精简写法

    上面 exec 的调用虽然很容易理解,但看上去有些冗长:参数类型满天飞fn 的参数 v1v2exec本身的前两个参数容易混淆。
    强大的 Swift 编译器允许我们对其做一些精简,下面一步步做介绍:

    • 由于在定义 exec 的时候已经明确了 fn 的两个参数类型Int和返回类型Int,所以可以做如下简化:

      // 只需要使用 in 关键字 将参数和函数体做区隔即可,省略了v1、v2的类型以及返回值
      exec(v1: 10, v2: 20, fn: { v1, v2 in
          return v1 + v2
      })
      
    • 就跟函数一样,闭包函数体的单行表达式可以省略 return

      // 省略了函数体的 return 关键字
       exec(v1: 10, v2: 20, fn: { v1, v2 in
           v1 + v2
       })
      
    • Swift 中可以使用 $0$1 来分别表示第0个参数第1个参数

      exec(v1: 10, v2: 20, fn: {
           $0 + $1
      })
    
    • 甚至于你可以省略$0$1(这种方式过分了,不推荐😓)
      // 直接使用一个 + ,表示第0个参数和第1个参数直接是加号运算符
      exec(v1: 10, v2: 20, fn: +)
    

    尾随闭包

    如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,将这个闭包替换成为尾随闭包的形式很有用。
    尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
    在使用尾随闭包时,你不用写出它的参数标签。

    还用之前的 exec 函数举例

    • 不使用尾随闭包:
    exec(v1: 10, v2: 20, fn: { (v1: Int, v2: Int) -> Int in
        return v1 + v2
    })
    
    • 使用尾随闭包:
      exec(v1: 10, v2: 20) { (v1: Int, v2: Int) -> Int in
         return v1 + v2
      }
      

    尾随闭包不仅仅省略了 fn 形参,而且将{函数体}挪到了()外面让整个函数调用更加的直观易读。同样可以对其进行精简:
    swift // 省略闭包参数类型和返回值 exec(v1: 10, v2: 20) { v1, v2 in // 省略 return v1 + v2 }
    然后:
    swift exec(v1: 10, v2: 20) { // 直接使用 $0、$1 表示第0和第1个参数 $0 + $1 }
    这个表达式就非常的简洁优雅了!👍🏻 注意,下面的表达式是不允许的:
    swift // 尾随闭包不允许省略$0、$1 exec(v1: 10, v2: 20) { + }

    闭包的值捕获

    闭包可以理解为函数以及其捕获的上下文中的变量或常量的总和

    看下面这个函数:

    func getFn() -> (Int) -> Int {
        var num = 0
        func plus (_ i: Int) -> Int {
            num = num + i
            return num
        }
        return plus
    }
    

    getFn函数没有参数,返回值为(Int) -> Int(一个参数为Int返回值为Int的函数)。

    getFn函数体内定义了一个Int类型的变量num,又定义了一个plus函数,并将其作为getFn函数的返回值返回。

    plus函数对num变量进行了捕获,构成了闭包。

    思考如下代码的输出:

    var f = getFn()
    print(f(1))
    print(f(2))
    print(f(3))
    print(f(4))
    

    结果是哪一组?

    1         1
    2   or    3
    3         6
    4         10
    

    正如前面提到的函数以及其捕获的上下文中的变量或常量的总和,当调用getFn()时,返回的不仅仅是plus函数同时也包括num变量组成的闭包整体!理解这个概念非常重要,因此getFn()返回的其实就是下面的代码片段:

    var num = 0
    func plus (_ i: Int) -> Int {
        num = num + i
        return num
    }
    

    因此f(1)``f(2)``f(3)``f(4)访问的是同一个num,或者说同一块变量内存

    num 初始值为 0
    f(1)就等价于 num = num + 1
    f(2)就等价于 num = num + 2
    f(3)就等价于 num = num + 3
    f(4)就等价于 num = num + 4
    

    所以结果为:

    1
    3
    6
    10
    

    再思考这种情况:

    var f1 = getFn()
    print(f1(1))   // 1
    print(f1(2))   // 3
    print(f1(3))   // 6
    
    var f2 = getFn()
    print(f2(1))   // 1
    print(f2(2))   // 3
    print(f2(3))   // 6
    

    很显然每创建一个getFn函数引用(没错,函数和闭包都是引用类型),Swift 都会为所捕获的num申请一份新的堆空间内存,来保证所有的f1访问的都是同一块内存地址,所有的f2访问的也都是同一块内存地址,但f1f2访问的num堆地址不是同一块!

    自动闭包

    观察下面这个函数:

    // 如果第1个参数大于0则返回之,否则返回第2个参数
    func getFirstPositiveNumber(n1: Int, n2: Int) -> Int {
        return n1 > 0 ? n1 : n2
    }
    

    调用getFirstPositiveNumber

    func getDoubleOfNumber(_ v: Int) -> Int {
        return v * 2
    }
    getFirstPositiveNumber(n1: 10, n2: 20)  // 10
    getFirstPositiveNumber(n1: -10, n2: 20) // 20
    

    上述函数看似很简单,但有一个隐患可以优化:

    如果n1 > 0,那么n2是什么根本不重要了,可是编译器还是需要花费开销去"关心"n2。你可能会不以为然,心理嘀咕『不就一个Int,至于么?"』

    那下面这个例子呢:

    func getNumber() -> Double {
        return Double.pi * 10.0
    }
    getFirstPositiveNumber(n1: 10.0, n2: getNumber())  // 10.0
    

    既然n1已经>0了,我们为何还要去调用getDoubleOfNumber来计算n2呢?

    如果getDoubleOfNumber函数 计算很复杂需要去读取本地数据 甚至 需要联网抓取数据 呢?这种浪费就不能不以为然了吧。

    那怎么解决呢?当时是使用闭包:

    func getFirstPositiveNumber(n1: Double, n2: () -> Double) -> Double {
        return n1 > 0 ? n1 : n2()
    }
    

    n2的类型从Double改为() -> Double,调用时:

    getFirstPositiveNumber(n1: 10.0, n2: {
        Double.pi * 10.00
    })
    

    或者使用尾随闭包:

    getFirstPositiveNumber(n1: 10) {
        Double.pi * 10.00
    }
    

    n1 > 0时,闭包的函数体Double.pi * 10.00根本不用执行!完美!

    可是每次调用getFirstPositiveNumber都要写闭包会很繁琐,因此Swift标准库提供了自动闭包的语法糖来解决这个问题,getFirstPositiveNumber函数只需要像下面这么写:

    func getFirstPositiveNumber1(n1: Double, n2: @autoclosure () -> Double) -> Double {
        return n1 > 0 ? n1 : n2()
    }
    getFirstPositiveNumber1(n1: 10, n2: Double.pi * 2)
    

    调用时n2会自动写成闭包的形式!

    逃逸闭包

    func fn1(_ closure: (Int) -> Int) {
        print(closure(10))
    }
    

    fn1函数只有一个闭包参数closure,且closurefn1函数体内部直接调用,这时候我们称closure为非逃逸闭包。

    如果像下面这么写编译器就会报错:

    var c: ((Int) -> Int)?
    func fn2(_ closure: (Int) -> Int) {
        c = closure
    }
    

    closurefn2作用域外调用,即成为逃逸闭包,可以使用@escaping关键词消除编译器报错:

    func fn2(_ closure: @escaping (Int) -> Int) {
        c = closure
    }
    

    相关文章

      网友评论

          本文标题:Swift 中的闭包

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