Swift 进阶(五)闭包

557 阅读9分钟

闭包表达式(Closure Expression)

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

闭包表达式格式如下

{
    (参数列表) -> 返回值类型 in
    函数体代码
}
var fn = {
    (v1: Int, v2: Int) -> Int in
    return v1 + v2
}

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

闭包表达式的简写如下

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

var fn: (Int, Int) -> Int = { $0 + $1 }
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
    print(fn(v1, v2))
}


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

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

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

exec(v1: 10, v2: 20, fn: {
    v1, v2 in return v1 + v2
})

exec(v1: 10, v2: 20, fn: {
    v1, v2 in v1 + v2
})

exec(v1: 10, v2: 20, fn: { $0 + $1 })

exec(v1: 10, v2: 20, fn: +)

尾随闭包

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

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

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

exec(v1: 10, v2: 20) {
    $0 + $1
}

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

func exec(fn: (Int, Int) -> Int) {
    print(fn(1, 2))
}

exec(fn: { $0 + $1 })
exec() { $0 + $1 }
exec { $0 + $1 }
exec { _, _ in 10 }

Swift中的sort函数用来排序的,使用的就是闭包的写法

-w449 -w597

var nums = [11, 2, 18, 6, 5, 68, 45]

nums.sort()

nums.sort {
    (i1, i2) -> Bool in
    i1 < i2
}

nums.sort(by: { (i1, i2) in return i1 < i2 })

nums.sort(by: { (i1, i2) in return i1 < i2 })

nums.sort(by: { (i1, i2) in i1 < i2 })

nums.sort(by: { $0 < $1 })

nums.sort(by: <)

nums.sort() { $0 < $1 }

nums.sort { $0 < $1 }


print(nums) // [2, 5, 6, 11, 18, 45, 68]

闭包(Closure)

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

  • 一般指定义在函数内部的函数
  • 一般它捕获的是外层函数的局部变量\常量
typealias Fn = (Int) -> Int

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

通过汇编分析闭包的实现

看下面示例代码,分别打印为多少

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

var fn = getFn()

print(fn(1)) // 1
print(fn(2)) // 3
print(fn(3)) // 6
print(fn(4)) // 10

我们通过反汇编来观察

-w1012

通过这句调用可以看出,在return plus之前,闭包底层会调用malloc函数进行堆内存的分配,也就是将拷贝num的值到堆上来持有不被释放,而栈里的num由于getFn调用完毕就随着栈释放了,plus函数里操作的都是堆上的num

调用malloc函数之前需要告诉系统要分配多少内存,需要24个字节来存储内存,而malloc函数分配的都是16的倍数,所以会分配32个字节内存

-w1014 -w596

我们打印rax寄存器的值可以知道,系统分配的32个字节,前16个字节用来存储其他信息,而且从图上的圈起来的地方也可以看到,将0移动16个字节,所以16个字节之后的8个字节才用来存储num的值

-w532

调用fn(1),将断点打在这里,然后查看反汇编指令

-w1009 -w575

然后调用到plus函数内部,再次打印rax寄存器的值,发现num的值已经变为1了

-w575

然后继续往下执行调用fn(2),发现num的值已经变为3了

-w606

然后继续往下执行调用fn(3),发现num的值已经变为6了

-w596

然后继续往下执行调用fn(4),发现num的值已经变为10了

闭包和类的相似之处

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

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

类似如下示例

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

var cs = Closure()
cs.plus(1)
cs.plus(2)
cs.plus(3)
cs.plus(4)

而且通过反汇编也能看出类和闭包的共同之处,分配的堆内存空间前16个字节都是用来存储基础信息和引用计数的

再看下面的示例

如果把num变成全局变量呢,还会不会分配堆内存

typealias Fn = (Int) -> Int

var num = 0

func getFn() -> Fn {

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

var fn = getFn()

print(fn(1)) // 1
print(fn(2)) // 3
print(fn(3)) // 6
print(fn(4)) // 10

我们通过反汇编可以看到,系统不再分配堆内存空间了

-w717

**注意:**如果返回值是函数类型,那么参数的修饰要保持统一

func add(_ num: Int) -> (inout Int) -> Void {
    func plus(v: inout Int) {
        v += num
    }
    
    return plus
}

var num = 5
add(20)(&num)

print(num)

自动闭包

我们先看下面的示例代码

如果调用getFirstPositive并传入两个参数,第一个参数符合条件,但是还需要调用plus来得到第二个参数,这种设计相比就稍许有些浪费了

func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
    v1 > 0 ? v1 : v2
}

func plus(_ num1: Int, _ num2: Int) -> Int {
    print("haha")
    return num1 + num2
}

getFirstPositive(10, plus(2, 4))

我们进行了一些优化,将第二个参数的类型变为函数,只有条件成立的时候才会去调用

func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
    v1 > 0 ? v1 : v2()
}

func plus(_ num1: Int, _ num2: Int) -> Int {
    print("haha")
    return num1 + num2
}

getFirstPositive(10, { plus(2, 4)} )

这样确定能够满足条件避免多余的调用,但是可读性就会差一些

我们可以使用自动闭包@autoclosure来修饰形参

@autoclosure会将传进来的类型包装成闭包表达式,这是编译器特性

@autoclosure只支持() -> T格式的参数

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
    v1 > 0 ? v1 : v2()
}

func plus(_ num1: Int, _ num2: Int) -> Int {
    print("haha")
    return num1 + num2
}

getFirstPositive(10, plus(2, 4))

@autoclosure并非只支持最后一个参数

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int, _ v3: Int) -> Int {
    v1 > 0 ? v1 : v2()
}

空合并运算符??中就使用了@autoclosure来将??后面的参数进行了包装

-w860

@autoclosure和无@autoclosure会构成函数重载,不会报错

func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
    v1 > 0 ? v1 : v2()
}

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
    v1 > 0 ? v1 : v2()
}

注意:为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行

通过汇编进行底层分析

1.分析下面这个函数的内存布局

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

var fn = sum
print(MemoryLayout.stride(ofValue: fn)) // 16

反汇编之后

-w717

可以看到底层会先计算sum的值,然后移动到fn的前8个字节,再将0移动到fn的后8个字节,总共占用16个字节

-w716

两个地址相差8个字节,所以是连续的,都表示fn的前后8个字节的地址值

2.分析下面这个函数的内存布局

typealias Fn = (Int) -> Int

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

var fn = getFn()

print(Mems.size(ofVal: &fn)) // 16

反汇编之后

-w716

我们先能看到调用getFn之后raxrdx会给fn分配16个字节

然后我们进入getFn看看raxrdx存储的值分别是什么

-w715

可以看到会将plus的返回值放到rax

-w949

可以看到ecx和自己进行异或运算,并把结果0存储到rdx

所以回过头看第一张图就知道了,fn的16个字节中,前8个字节存储的是plus的返回值,后8个字节存储的是0

等同于将plus函数赋值给fn

var fn = plus()

3.分析下面这个函数的内存布局

我们将上面示例里的plus函数内部对num进行捕获,看看其内存布局有什么变化

typealias Fn = (Int) -> Int

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

var fn = getFn()

fn(1)
fn(2)
fn(3)

print(Mems.size(ofVal: &fn)) // 16

反汇编之后

-w945

我们可以看到,调用完getFn之后,会分别将raxrdx的值移动到rip+内存地址,也就是给全局变量fn进行赋值操作

我们通过打印获取fn的内存占用知道是16个字节,fn的前8个字节就是rax里存储的值,而后8个字节存储的是rdx里的值

我们只需要找到raxrdx里分别存储的是什么就可以了

-w947

可以看到在堆空间分配完内存之后的rax给上面几个都进行了赋值,最后的rdx里存储的就是堆空间的地址值

-w944

从这句看rax里存储的应该是和plus函数相关,下面我们就要找到rax里存储的是什么

-w947 -w946

而且我们调用fn(1)时也可以推导出是调用的全局变量fn的前八个字节

-w947

参数1会存储到edi

而经过上面的推导我们知道-0xf8(%rbp)中存储的是fn的前8个字节,那么往后8位就是-0x100(%rbp),里面放的肯定就是堆空间的地址值了,存储到了r13

我们在这里打断点,来观察rax里到底存储的是什么

-w947 -w946 -w947 -w947 -w949

经过一系列的跳转,重要来到了plus真正的函数地址

而且r13最后给了rsirdi中存储的还是参数1

-w947 -w946

进到plus函数中,然后找到进行相加计算的地方,因为传进来的参数是变化的,所以不可能是和固定地址值进行相加

-w947 -w946

通过推导得知rcx里存储的值就是rdi中的参数1

-w945 -w945

通过推导得知rdx里存储的值就是rsi中的堆内存的num地址

所以可以得知0x10(%rdx)也就是rdx跳过16个字节的值就是num的值

-w741

通过打印也可以证明我们的分析是正确的

-w947 -w946

通过推导可以发现rax中存储的是rsi的num的地址值

然后将rcx中的值覆盖掉rax中的num地址值

而且真正进行捕获变量的时机是在getFn即将return之前做的事

4.分析下面这个函数的内存布局

我们来看下面这个闭包里的变量会被捕获几次

typealias Fn = (Int) -> (Int, Int)

func getFns() -> (Fn, Fn) {
    var num1 = 0
    var num2 = 0

    func plus(_ i: Int) -> (Int, Int) {
        num1 += i // 6 + 0 = 6, 1 + 4 = 5,
        num2 += i << 1 // 1100 = 12 + 0 = 12, 1000 = 8 + 2 = 10
        return (num1, num2)
    }

    func minus(_ i: Int) -> (Int, Int) {
        num1 -= i // 6 - 5 = 1, 5 - 3 = 2
        num2 -= i << 1 // 1010 = 12 - 10 = 2, 0110 = 10 - 6 = 4
        return (num1, num2)
    }

    return (plus, minus)
}

let (p, m) = getFns()
print(p(6)) // 6, 12
print(m(5)) // 1, 2
print(p(4)) // 5, 10
print(m(3)) // 2, 4

反汇编之后

-w946

发现其底层分别会分配两个堆空间,但是num1、num2也只是分别捕获一次,然后两个函数plus、minus共有