Swift系列八 - 闭包

2021-05-27 14:28:02 浏览数 (1)

什么是闭包?闭包表达式又是什么?

一、闭包表达式(Closure Expression)

在Swift中,可以通过func定义一个函数,也可以通过闭包表达式定义一个函数。

1.1. 闭包表达式的格式

代码语言:javascript复制
{
  (参数列表) -> 返回值类型 in
  函数体代码
}

1.2. 闭包表达式和函数的比较

定义一个普通的函数:

代码语言:javascript复制
func sum(_ v1: Int, _ v2: Int) -> Int { v2   v2 }
let result = sum(10, 20)
print(result)
// 输出:30

定义闭包:

代码语言:javascript复制
var sum = {
    (v1: Int, v2: Int) -> Int in
    return v1   v2
}
let result = sum(10, 20)
print(result)
// 输出:30

1.3. 闭包表达式的简写

定义函数:

代码语言:javascript复制
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}

要想使用exec函数,则必须传入两个Int类型的参数和一个返回Int类型的函数,然后exec内部执行了传入的函数。

代码语言:javascript复制
func sum(_ a: Int, _ b: Int) -> Int {
    return a   b
}

exec(v1: 10, v2: 20, fn: sum)
// 输出:30

按照以往的知识,我们需要定义一个函数,然后把函数传给exec就行了。其实我们也可以使用闭包表达式。

代码语言:javascript复制
exec(v1: 10, v2: 20, fn: {
    (v1: Int, v2: Int) -> Int in
    return v1   v2
})
// 输出:30

上面的闭包表达式还可以简写:

1.3.1. 简写一
  • 省略参数类型和返回值;
  • 编译器会自动推断闭包表达式中参数类型和返回值类型。
代码语言:javascript复制
exec(v1: 10, v2: 20, fn: {
    v1, v2 in return v1   v2
})
// 输出:30
1.3.2. 简写二

如果函数的返回值是一个单一表达式,可以省略return

代码语言:javascript复制
exec(v1: 10, v2: 20, fn: {
    v1, v2 in v1   v2
})
// 输出:30
1.3.3. 简写三

如果闭包表达式不想写参数,可以使用美元符$序号代替,序号从0开始,代表参数位置。

代码语言:javascript复制
exec(v1: 10, v2: 20, fn: { $0   $1 })
// 输出:30
1.3.4. 简写四(不建议)

简单的闭包表达式还可以直接使用运算符代替。

代码语言:javascript复制
exec(v1: 10, v2: 20, fn:  )
// 输出:30

二、尾随闭包

2.1. 特点一(最后一个实参)

如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性。

尾随闭包是一个被书写在函数调用括号外面(后面)的闭包表达式。

以调用上面的exec函数为例:

代码语言:javascript复制
exec(v1: 10, v2: 20) {
  $0   $1
}
// 输出:30

2.2. 特点二(唯一实参)

如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后边写圆括号。

定义函数:

代码语言:javascript复制
func exec(fn: (Int, Int) -> Int) {
    print(fn(10, 20))
}

调用方式:

代码语言:javascript复制
// 方式一:
exec(fn: { $0   $1 })

// 方式二:
exec() { $0   $1 }

// 方式三:
exec { $0   $1 }

/*
 输出:
 30
 30
 30
 */

三、闭包(Closure)

闭包和闭包表达式严格意义上来讲并不是同一个概念。

一个函数和它所捕获的变量/常量环境组合起来,称为闭包。

  • 一般指定义在函数内部的函数;
  • 一般它捕获的是外层函数的局部变量/常量。

示例代码:

代码语言:javascript复制
typealias Fn = (Int) -> Int
func getFn() -> Fn {
    var num = 0
    func plus(_ i: Int) -> Int {
        num  = 1
        return num
    }
    return plus
}
var fn = getFn()
print(fn(1))
print(fn(2))
print(fn(3))
print(fn(4))
/*
 输出:
 1
 3
 6
 10
 */

为什么var num = 0作为局部变量还能一直累加?不是应该在函数执行完成后就被释放了么?我们通过汇编一探究竟。

3.1. 汇编分析闭包

3.1.1. 如果内部函数没有捕获外部变量:

通过分析可以看到,函数返回的是一个地址,也就是变量fn里面存放的是函数地址。

3.1.2. 如果内部函数捕获外部变量:

汇编代码就变得复杂一点了,并且出现了swift_allocObject关键字,也就意味着在堆空间申请了一块内存,内存存放的是num的值。每次调用fn,访问的num都是同一块内存地址,所以才会出现局部变量也能一直累加的效果。

3.1.3. 证明swift_allocObject存放的是num

第一步:源代码断点:

第二步:查看swift_allocObject返回的地址:

第三步:查看rax地址存放的初始化值:

第四步:执行fn(1)后:

第五步:执行fn(2)后:

结论: 内部函数一旦捕获了外部的局部变量,要想持续使用这个变量,就需要延迟变量的生命周期,所以在堆空间分配一块内存来存放局部变量的值。

思考:为什么可以访问同一块内存空间? var fn = getFn()fn占用16个字节,前8个字节存放返回的函数地址(plus的封装),后8个字节存放堆空间(num)的地址。如果var fn2 = getFn()fn1fn2前8个字节可能相同,不同的是后面的8个字节。

3.2. 闭包和类的比较

可以把闭包想象成是一个类的实例对象。

  • 内存在堆空间;
  • 捕获的局部变量/常量就是对象的成员(存储属性);
  • 组成闭包的函数就是类内部定义的方法。
代码语言:javascript复制
class Closure {
    var num = 0
    func plus(_ i: Int) -> Int {
        num  = i
        return num
    }
}
var cs = Closure()
print(cs.plus(1))
print(cs.plus(2))
print(cs.plus(3))
print(cs.plus(4))
/*
 输出:
 1
 3
 6
 10
 */

四、自动闭包

示例代码:

代码语言:javascript复制
// 如果第一个数大于0,返回第一个数,否则返回第二个数
func getFirst(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}
getFirst(10, 20) // 10
getFirst(-2, 20) // 20
getFirst(0, -4) // -4

把上面的代码修改如下:

代码语言:javascript复制
func getNumber() -> Int {
    print("getNumber")
    let a = 10
    let b = 10
    return a   b
}
let result1 = getFirst(10, getNumber())
print(result1)
/*
 输出:
 getNumber
 10
 */

let result2 = getFirst(-1, getNumber())
print(result2)
/*
 输出:
 getNumber
 20
 */

分析:不管第一个数是否大于0,都会执行第二个参数传入的函数,这样整体有点浪费(性能/空间)。我们可以尝试把函数第二个入参类型修改为函数类型。

优化代码:

代码语言:javascript复制
typealias VoidFunc = () -> Int
func getFirst(_ v1: Int, _ v2: VoidFunc) -> Int {
    print("getFirst")
    return v1 > 0 ? v1 : v2()
}

func getNumber() -> Int {
    print("getNumber")
    let a = 10
    let b = 10
    return a   b
}
getFirst(10, getNumber)
/*
 输出:
 getFirst
 */

getFirst(-1, getNumber)
/*
 输出:
 getFirst
 getNumber
 */

结果:只有需要的时候才会执行对应的代码。

但是,如果这样修改后,每次都需要传入一个函数会有点麻烦。Swift提供了自动闭包功能,可以把普通变量自动包裹成闭包,这样就能满足上面代码的所有的功能了。

关键字: @autoclosure 用法:在函数前面加上@autoclosure关键字即可。

自动闭包代码:

代码语言:javascript复制
typealias VoidFunc = () -> Int
func getFirst(_ v1: Int, _ v2: @autoclosure VoidFunc) -> Int {
    print("getFirst")
    return v1 > 0 ? v1 : v2()
}

getFirst(10, 20) // 10
getFirst(-1, 10) // 10

自动闭包特点

  • @autoclosure会将普通参数(例如,20)封装成闭包{ 参数 }(例如,{ 20 });
  • @autoclosure只支持() -> T(无参有返回值)格式的参数;
  • @autoclosure并非只支持最后一个参数,和位置没有任何关系;
  • @autoclosure、无@autoclosure,构成函数重载;
  • 为了避免与期望冲突,使用了有@autoclosure的地方最好明确注释清楚:这个值会被延迟执行(有可能不执行)。

延伸: 空合并运算符??使用了@autoclosure技术。

代码语言:javascript复制
public func ?? <t>(optional: T?, defaultValue: @autoclosure () throws -&gt; T?) rethrows -&gt; T?

五、应用

通过数组的排序看下闭包表达式是如何使用的。

定义函数:

代码语言:javascript复制
var arr = [20, 52, 19, 3, 80, 72]

3.1. 系统排序

在Swift中,Array为开发者提供了sort()排序函数,开发者可以直接使用。

代码语言:javascript复制
arr.sort()
print(arr)
// 输出:[3, 19, 20, 52, 72, 80]

3.2. 自定义排序

sort()是升序的,如果要降序呢?我们可以使用另外一个函数进行自定义排序。

Array提供的函数:

代码语言:javascript复制
func sort(by areInIncreasingOrder: (Element, Element) throws -&gt; Bool) rethrows

可以看到,该函数让传入一个闭包表达式。使用规则如下:

  • 返回true:第一个元素排在第二个元素前面;
  • 返回false:第一个元素排在第二个元素后面。

调用方式一(普通函数):

代码语言:javascript复制
func compare(i1: Int, i2: Int) -&gt; Bool {
  return i1 &gt; i2
}
arr.sort(by: compare)
print(arr)
// 输出:[80, 72, 52, 20, 19, 3]

调用方式二(闭包表达式):

代码语言:javascript复制
arr.sort(by: {
    (i1: Int, i2: Int) -&gt; Bool in
    return i1 &gt; i2
})
arr.sort(by: { i1, i2 in return i1 &gt; i2 })
arr.sort(by: { i1, i2 in i1 &gt; i2 })
arr.sort(by: { $0 &gt; $1 })
arr.sort(by: &gt;)
arr.sort() { $0 &gt; $1 }
arr.sort { $0 &gt; $1 }
// 输出:[80, 72, 52, 20, 19, 3]

0 人点赞