站在汇编角度深入了解 Swift(六)

351 阅读4分钟

闭包表达式

  • 在 swift 中,可以通过 func 定义一个函数,也可以闭包表达式定义一个函数
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}

var fn: (Int, Int) -> Int = { i, j in
    return i + j
}
exec(v1: 1, v2: 3, fn: fn)
exec(v1: 1, v2: 3) { (i, j) -> Int in
    return i + j
}
exec(v1: 1, v2: 2) { $0 + $1 }
exec(v1: 1, v2: 2, fn: +)

尾随闭包

  • 如果将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强函数的可读性
    • 尾随闭包是一个被书写在函数调用括号外面的闭包表达式
    • 如果闭包表达式是函数的唯一实参,而且使用了尾随闭包的语法,那就不需要在函数名后面写圆括号
exec(v1: 1, v2: 3) { (i, j) -> Int in
    return i + j
}
// 下面这个就可以不写括号
// 像 array 里面的 reduce、filter、sort、map 等等
exec { v1 + v2 }

示例: sort 的用法

var a = [1, 3, 2, 8, 7, 5]
var b = a
a.sort(by: <)
b.sort { $0 < $1 }

闭包(和闭包表达式完全是两回事)

  • 一个函数和它所捕获的变量/常量组合起来,称为闭包
    • 一般指定义在函数内部的函数
    • 一般捕获的是外层函数的局部变量/常量
    • 这个和 oc 中的 block 就完全不是一个概念了,swift 中如果不捕获外层的局部变量/常量的话,其实不能算是闭包,只能算是一个函数或者方法
swiftstudy`test18():
typealias Fn = (Int) -> Int
func getFn() -> Fn {
    // 局部变量
    var num = 1
    func plus(_ i: Int) -> Int {
        num += i
        return num
    }
    return plus 
}// 返回的 plus 和 num 形成了闭包

let fn = getFn()
print(fn(1))
print(fn(1))
print(fn(1))
print(fn(1))

------------------执行结果---------------------
2
3
4
5

------------------汇编分析---------------------
swiftstudy`getFn #1 () in test17():
movl   $0x18, %esi // swift_allocObject(24),堆是以16字节为基本单位,所以实际分配的内存为32个字节
movl   $0x7, %edx
callq  0x100006c5a               ; symbol stub for: swift_allocObject
movq   %rax, %rdx
addq   $0x10, %rdx
movq   %rdx, %rsi
movq   $0x1, 0x10(%rax)
callq  0x100006cae               ; symbol stub for: swift_retain
callq  0x100006ca8               ; symbol stub for: swift_release
leaq   0x2b58(%rip), %rax        ; partial apply forwarder for plus

由上面的汇编就可以看出来,实际上在捕捉外层变量/常量的时候,会分配堆内存。这样的话生命周期就被延长了。所以后面也就有对应的 retain 和 release。这也就可以解释为什么执行的结果是 2345,而不是 2222 了。

我们可以分析一下,为什么说不捕获外层的变量/常量的时候就不能称为闭包

swiftstudy`test18():
typealias Fn = (Int) -> Int
func getFn() -> Fn {
    func plus(_ i: Int) -> Int {
        return i + 1
    }
    return plus
}
let fn = getFn()
print(fn(1))

------------------汇编分析---------------------
swiftstudy`getFn #1 () in test18():
pushq  %rbp
movq   %rsp, %rbp
leaq   0x14f5(%rip), %rax        ; plus #1 (Swift.Int) -> Swift.Int in getFn #1 () -> (Swift.Int) -> Swift.Int in swiftstudy.test18() -> () at main.swift:275
xorl   %ecx, %ecx
movl   %ecx, %edx
popq   %rbp
retq   

可以看出来,跟普通的函数调用没啥区别,根据没有分配堆空间的过程。所以不能算是闭包。

把闭包想像成一个类的实例对象

  • 内存在堆空间
  • 捕获的局部变量/常量就是对象的成员
  • 组成闭包的函数就是类内部定义的方法

上面的代码可以完全改写成下面的这样的形式

class Closure {
    var num = 1
    func plus(_ i: Int) -> Int {
        num += i
        return num 
    }
}
let c = Closure()
c.plus(1)
c.plus(1)
c.plus(1)
c.plus(1)

------------------执行结果---------------------
2
3
4
5

自动闭包

func getNum() -> Int {
    let a = 10
    let b = 11
    print("getNum")
    return a + b
}

@discardableResult
func max(a: Int, b: Int) -> Int {
    return a > 0 ? a : b
}

print(max(a: 10, b: getNum()))

------------------执行结果---------------------
getNum
21

可以看出来上面的问题就是不管 a 是不是大于0,那么 getNum() 都是会被调用的。应该如何优化呢?

@discardableResult
func max1(a: Int, b: () -> Int) -> Int {
    return a > 0 ? a : b()
}
print(max1(a: 10, b: getNum))

这样的话,b就不会执行了,如果这个 b 的闭包表达式非常简单,这种方式就不是很优雅,所以 swift 给我们提供了一种更优雅的实现方式,就是 @autoclosure

  • @autoclosure 会自动将 20 封装成闭包 { 20 }
  • @autoclosure 只支持 () -> T 格式的参数
  • @autoclosure 并非只支持最后一个参数
  • 空合并运算符就是利用了 @autoclosure 技术
  • 有 @autoclosure 和无 @autoclosure 构成了重载
    • 如果有 @autoclosure 你把 20 写成 { 20 } 他会报错
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> ??
@discardableResult
func max2(a: Int, b: @autoclosure () -> Int) -> Int {
    return a > 0 ? a : b()
}
print(max2(a: 10, b: getNum()))

思考

  1. 局部变量/常量是什么时候被捕获的?

    是在 return plus 的时候,所以你只要在 return plus 之前修改了 num 的值,那么捕获的其实就是你修改之后的 num 值,如果你 return { $0 },那么它其实也不会捕获 num 的值。

  2. 那么如果是捕捉的是全局变量,是闭包还是函数?

    是函数。都是全局变量了,内存是在数据段,你还在堆内存开辟是脑子有问题吗?局部变量会没命,所以需要开辟堆空间保命,全局就不凑这个热闹了。

  3. 执行结果是什么?

var functions: [() -> Int] = []
for i in 1...3 {
    functions.append { i }
}
for f in functions {
    print(f())
}
123