闭包表达式(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函数
用来排序的,使用的就是闭包的写法
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
我们通过反汇编来观察
通过这句调用可以看出,在return plus
之前,闭包底层会调用malloc函数
进行堆内存的分配,也就是将拷贝num的值到堆上来持有不被释放,而栈里的num由于getFn
调用完毕就随着栈释放了,plus函数
里操作的都是堆上的num
调用malloc函数
之前需要告诉系统要分配多少内存,需要24个字节来存储内存,而malloc函数
分配的都是16的倍数,所以会分配32个字节内存
我们打印rax寄存器
的值可以知道,系统分配的32个字节,前16个字节用来存储其他信息,而且从图上的圈起来的地方也可以看到,将0移动16个字节,所以16个字节之后的8个字节才用来存储num的值
调用fn(1)
,将断点打在这里,然后查看反汇编指令
然后调用到plus函数
内部,再次打印rax寄存器
的值,发现num的值已经变为1了
然后继续往下执行调用fn(2)
,发现num的值已经变为3了
然后继续往下执行调用fn(3)
,发现num的值已经变为6了
然后继续往下执行调用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
我们通过反汇编可以看到,系统不再分配堆内存空间了
**注意:**如果返回值是函数类型,那么参数的修饰要保持统一
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
来将??
后面的参数进行了包装
有@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
反汇编之后
可以看到底层会先计算sum的值,然后移动到fn的前8个字节,再将0移动到fn的后8个字节,总共占用16个字节
两个地址相差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
反汇编之后
我们先能看到调用getFn
之后rax
和rdx
会给fn分配16个字节
然后我们进入getFn
看看rax
和rdx
存储的值分别是什么
可以看到会将plus的返回值
放到rax
中
可以看到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
反汇编之后
我们可以看到,调用完getFn
之后,会分别将rax
和rdx
的值移动到rip+内存地址
,也就是给全局变量fn进行赋值操作
我们通过打印获取fn的内存占用知道是16个字节,fn的前8个字节就是rax
里存储的值,而后8个字节存储的是rdx
里的值
我们只需要找到rax
和rdx
里分别存储的是什么就可以了
可以看到在堆空间分配完内存之后的rax
给上面几个都进行了赋值,最后的rdx
里存储的就是堆空间的地址值
从这句看rax
里存储的应该是和plus函数
相关,下面我们就要找到rax
里存储的是什么
而且我们调用fn(1)时也可以推导出是调用的全局变量fn的前八个字节
参数1会存储到edi
中
而经过上面的推导我们知道-0xf8(%rbp)
中存储的是fn的前8个字节,那么往后8位就是-0x100(%rbp)
,里面放的肯定就是堆空间的地址值了,存储到了r13
中
我们在这里打断点,来观察rax
里到底存储的是什么
经过一系列的跳转,重要来到了plus真正的函数地址
而且r13
最后给了rsi
,rdi
中存储的还是参数1
进到plus函数
中,然后找到进行相加计算的地方,因为传进来的参数是变化的,所以不可能是和固定地址值进行相加
通过推导得知rcx
里存储的值就是rdi
中的参数1
通过推导得知rdx
里存储的值就是rsi
中的堆内存的num地址
所以可以得知0x10(%rdx)
也就是rdx
跳过16个字节的值就是num的值
通过打印也可以证明我们的分析是正确的
通过推导可以发现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
反汇编之后
发现其底层分别会分配两个堆空间,但是num1、num2也只是分别捕获一次,然后两个函数plus、minus
共有