美文网首页
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-闭包

    Swift 闭包 函数 ()->() Swift 中的闭包和 Objective-C 中的 block 类似,闭包...

  • Swift闭包和函数

    函数在Swift中只是一种特殊的闭包,闭包在Swift语言中是一等公民,支持闭包嵌套和闭包传递。Swift中的闭包...

  • swift4 闭包

    swift 闭包 闭包:swift 中 函数是闭包的一种类似于oc的闭包闭包表达式(匿名函数) -- 能够捕获上下...

  • Swift学习笔记(1)

    SWift学习笔记 闭包 闭包表达式 闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 ...

  • swift中的闭包

    swift中的闭包 闭包是自包含的函数代码块,可以在代码中被传递和使用。swift中的闭包与C和Objective...

  • Swift总结

    1.闭包 swift中的闭包类似于oc中的block回调,但是swift的闭包有很多种写法,具有多变性,今天就来总...

  • swift 闭包(闭包表达式、尾随闭包、逃逸闭包、自动闭包)

    闭包是自含的函数代码块,可以在代码中被传递和使用 闭包和swift的对比 Swift 中闭包与OC的 block ...

  • 闭包

    闭包 本节内容包括: 闭包表达式 尾随闭包 值捕获 闭包是引用类型 Swift 中的闭包与 C 和 Objecti...

  • Swift入门基础5——闭包

    何为闭包? Swift中的闭包和OC中的block很相似(其实也有其他语言有闭包的概念)。所谓闭包,就是可以捕获其...

  • swift学习笔记 ⑥ —— 闭包

    Swift学习笔记 - 文集 闭包,就是能够读取其他函数内部变量的函数。Swift 中的闭包与 C 和 OC 中的...

网友评论

      本文标题:Swift 中的闭包

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