闭包表达式
- 在Swift中可以通过
func
定义一个函数,也可以通过闭包表达式
来定义函数;
import Foundation
func test1() -> Void {
//闭包表达式 用一个常量来接收
let fn = {
(v1: Int,v2: Int) -> Int in
return v1 + v2
}
print(fn(10, 20)) //30
}
func test2() -> Void {
let sum = {
(v1: Int,v2: Int) -> Int in
return v1 + v2
}(20,30) //匿名的闭包表达式 直接调用
print(sum) //50
}
test1()
test2()
- 闭包表达式的定义格式:
{
(参数列表) -> 返回值类型 in
函数体代码
}
闭包表达式的简写
- 定义一个函数exec,最后一个参数为函数,也就是闭包表达式,如下所示:
import Foundation
func sum(v1: Int,v2: Int) -> Int {
v1 + v2
}
func exec(v1: Int,v2: Int,fn: (Int,Int) -> Int) -> Void {
print(fn(v1,v2))
}
- 调用exec函数时,最后一个实参的传递方式有如下几种写法:
- 第一种方式:函数实参传入 函数名
exec(v1: 100, v2: 200, fn: sum)
- 第二种方式:函数实参 传入闭包表达式
exec(v1: 100, v2: 200, fn: { (v1: Int,v2: Int) -> Int in
return v1 + v2
})
- 第三种方式:函数实参 传入闭包表达式 尾随闭包
exec(v1: 100, v2: 200) { (v1: Int, v2: Int) -> Int in
return v1 + v2
}
- 第四种方式:尾随闭包 省略
参数类型
stu.exec(v1: 100, v2: 200) { (v1, v2) -> Int in
return v1 + v2
}
- 第五种方式:尾随闭包 省略
参数类型
,返回值类型
return关键字
,箭头
exec(v1: 100, v2: 200) { (v1, v2) in
v1 + v2
}
- 第六种方式:尾随闭包 采用美元符号,最简便的写法
stu.exec(v1: 100, v2: 200) {
$0 + $1
}
尾随闭包
- 如果将一个很长的闭包表达式作为函数的
最后一个实参
,使用尾随闭包可以增强函数的可读性; - 尾随闭包是一个被写在
函数调用括号外面的
闭包表达式;
func sum(v1: Int,v2: Int) -> Int {
v1 + v2
}
func exec(v1: Int,v2: Int,fn: (Int,Int) -> Int) -> Void {
print(fn(v1,v2))
}
exec(v1: 100, v2: 200, fn: sum)
//尾随闭包
exec(v1: 100, v2: 200) { (v1, v2) -> Int in
v1 + v2
}
//尾随闭包
exec(v1: 100, v2: 200) {
$0 + $1
}
- 如果闭包表达式是
函数唯一的实参
,而且使用了尾随闭包
的语法,那就不需要在函数名后面写圆括号了
;
func exme(fn: (Int,Int) -> Int) -> Void {
print(fn(1,2))
}
exme(fn: {$0 + $1})
exme(){$0 + $1}
exme{$0 + $1}
数组的排序
func ArrSort() -> Void {
var arr = [23,12,45,22,100,67]
arr.sort(by: cmp)
//完整写法
arr.sort(by: { (v1: Int,v2: Int) -> Bool in return v1 > v2})
//省略参数括号与返回值
arr.sort(by: {v1,v2 in return v1 > v2})
//省略参数括号与返回值,return
arr.sort(by: {v1,v2 in v1 > v2})
arr.sort(by: {$0 > $1})
arr.sort(by: >)
arr.sort(){$0 > $1}
arr.sort{$0 > $1}
print(arr)
}
//排序规则函数
func cmp(v1: Int,v2: Int) -> Bool {
//从大到小
return v1 > v2
}
ArrSort()
闭包
- 一个函数和它所捕获的常量/变量 环境组合起来,称为闭包;
- 一般指定义在函数内部的函数;
- 一般它捕获的是外层函数的常量/变量;
import Foundation
typealias Fn = (Int) -> Int
func getFn() -> Fn {
func plus(_ v1: Int) -> Int{
return v1
}
return plus
}
var fn = getFn()
- 当断点停在
return plus
所在代码行时,其汇编代码如下:
Swift11_闭包`getFn():
0x100003f70 <+0>: pushq %rbp
0x100003f71 <+1>: movq %rsp, %rbp
-> 0x100003f74 <+4>: leaq 0x15(%rip), %rax ; plus #1 (Swift.Int) -> Swift.Int in Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at main.swift:96
0x100003f7b <+11>: xorl %ecx, %ecx
0x100003f7d <+13>: movl %ecx, %edx
0x100003f7f <+15>: popq %rbp
0x100003f80 <+16>: retq
- 其中rax寄存器中存储着
plus
函数的内存地址,因为rax寄存器通常
用来存放函数的返回值; - LLDB输入指令
register read rax
,可以看到rax寄存器中存储内容为0x0000000100003f90
-
leaq 0x15(%rip), %rax
是将(rip + 0x15)得到的地址值赋值给rax寄存器,其中rip = 0x100003f7b,那么(0x100003f7b+ 0x15) = 0x0000000100003f90,就是上面rax寄存器中的值;
typealias Fn = (Int) -> Int
func getFn() -> Fn {
//局部变量
var num = 0
func plus(_ v1: Int) -> Int{
num += v1
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
所在代码行时,其汇编代码如下:
Swift11_闭包`getFn():
0x100003d70 <+0>: pushq %rbp
0x100003d71 <+1>: movq %rsp, %rbp
0x100003d74 <+4>: subq $0x20, %rsp
0x100003d78 <+8>: movq $0x0, -0x8(%rbp)
0x100003d80 <+16>: leaq 0x291(%rip), %rdi
0x100003d87 <+23>: movl $0x18, %esi
0x100003d8c <+28>: movl $0x7, %edx
0x100003d91 <+33>: callq 0x100003f14 ; symbol stub for: swift_allocObject
0x100003d96 <+38>: movq %rax, %rcx
0x100003d99 <+41>: addq $0x10, %rcx
0x100003d9d <+45>: movq %rcx, -0x8(%rbp)
0x100003da1 <+49>: movq $0x0, 0x10(%rax)
-> 0x100003da9 <+57>: movq %rax, %rdi
0x100003dac <+60>: movq %rax, -0x10(%rbp)
0x100003db0 <+64>: callq 0x100003f32 ; symbol stub for: swift_retain
0x100003db5 <+69>: movq -0x10(%rbp), %rdi
0x100003db9 <+73>: movq %rax, -0x18(%rbp)
0x100003dbd <+77>: callq 0x100003f2c ; symbol stub for: swift_release
0x100003dc2 <+82>: leaq 0x137(%rip), %rax ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
0x100003dc9 <+89>: movq -0x10(%rbp), %rdx
0x100003dcd <+93>: addq $0x20, %rsp
0x100003dd1 <+97>: popq %rbp
0x100003dd2 <+98>: retq
-
symbol stub for: swift_allocObject
看到这,就知道堆区分配了内存空间,创建了实例对象
,我们可以大胆猜测此堆区的实例对象主要是用来存储局部变量num的; -
0x100003d96 <+38>: movq %rax, %rcx
,rax寄存器存储了堆区分配的实例对象的内存地址; - LLDB输入
register read rax
,得到堆区的内存地址0x0000000100705570
-
x/4gx 0x0000000100705570
读取它的内容:
- 前面16个字节存储的是其他内容,往后的8个字节存储的是num的数值;
- 后面调用了四次fn函数,那么每次调用传参,都会相加然后存到堆区内存中,所以num值会递增;
- 返回的plus函数与在堆区分配内存空间存储局部变量,两者组成了闭包这种结构;
总结:
- 可以把闭包想象成一个类的实例对象;
- 内存在堆空间;
- 捕获的常量/变量就是对象的成员;
- 组成闭包的函数就是类内部定义的方法;
汇编分析闭包
import Foundation
typealias Fn = (Int) -> Int
func getFn() -> Fn {
func plus(_ i: Int) -> Int{
return i
}
return plus
}
var fn = getFn()
print(MemoryLayout.size(ofValue: fn)) //16个字节
fn(1)
fn(2)
- 根据
MemoryLayout.size(ofValue: fn)
可知闭包fn变量占用16个字节; - 当断点停在
var fn = getFn()
所在的代码行,汇编代码如下:
Swift11_闭包`main:
0x100003ad0 <+0>: pushq %rbp
0x100003ad1 <+1>: movq %rsp, %rbp
0x100003ad4 <+4>: pushq %r13
0x100003ad6 <+6>: subq $0xe8, %rsp
0x100003add <+13>: movl %edi, -0x54(%rbp)
0x100003ae0 <+16>: movq %rsi, -0x60(%rbp)
0x100003ae4 <+20>: callq 0x100003d30 ; Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at main.swift:95
-> 0x100003ae9 <+25>: movq 0x518(%rip), %rcx ; (void *)0x00007fff80cc5020: type metadata for Any
0x100003af0 <+32>: addq $0x8, %rcx
0x100003af7 <+39>: movq %rax, 0x456a(%rip) ; Swift11_闭包.fn : (Swift.Int) -> Swift.Int
0x100003afe <+46>: movq %rdx, 0x456b(%rip) ; Swift11_闭包.fn : (Swift.Int) -> Swift.Int + 8
0x100003b05 <+53>: movl $0x1, %edi
0x100003b0a <+58>: movq %rcx, %rsi
-
callq 0x100003d30
:根据后面注释,可以知道是在调用getFn
函数; -
movq %rax, 0x456a(%rip)
:rax寄存器存放的是getFn
函数的返回也就是plus函数的内存地址,然后将plus函数的内存地址写入到(rip+ 0x456a)内存地址中,由右侧的注释可知,(rip+ 0x456a)=(0x100003afe+ 0x456a) = 0x100008068是全局变量fn的内存地址;由上面知道fn占16个字节,现在只写入了8字节plus函数的内存地址,还有8个字节存放什么呢? - LLDB执行si指令,进入到
getFn
函数内部,汇编代码如下:
Swift11_闭包`getFn():
-> 0x100003d30 <+0>: pushq %rbp
0x100003d31 <+1>: movq %rsp, %rbp
0x100003d34 <+4>: leaq 0x15(%rip), %rax ; plus #1 (Swift.Int) -> Swift.Int in Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at main.swift:96
0x100003d3b <+11>: xorl %ecx, %ecx
0x100003d3d <+13>: movl %ecx, %edx
0x100003d3f <+15>: popq %rbp
0x100003d40 <+16>: retq
leaq 0x15(%rip), %rax
:可以看出plus函数的地址写入rax寄存器;xorl %ecx, %ecx
:按位异或,相同的位置为0,不同的位置为1,ecx和ecx的每一位都相同,所以相当于清零,即ecx中写入0;movl %ecx, %edx
:edx写入0,等价于rdx写入0;getFn
函数调用完成之后,回到main函数;movq %rdx, 0x456b(%rip)
:rdx寄存器中为0,将0写入(rip+ 0x456b)=(0x100003b05+ 0x456b) = 0x100008070,由右侧的注释可知0x100008070是全局变量fn的后8个字节,即fn+8;所以全局变量fn占16个字节空间,前8个字节存储plus函数地址,后8个字节存储0;
将上述代码做如下修改:
import Foundation
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 0
func plus(_ i: Int) -> Int{
num += i
return num
}
return plus
}
var fn = getFn()
print(MemoryLayout.stride(ofValue: fn)) //16个字节
fn(1)
fn(2)
- 当断点停在
var fn = getFn()
所在的代码行,汇编代码如下:
Swift11_闭包`main:
0x100003940 <+0>: pushq %rbp
0x100003941 <+1>: movq %rsp, %rbp
0x100003944 <+4>: pushq %r13
0x100003946 <+6>: subq $0xe8, %rsp
0x10000394d <+13>: movl %edi, -0x54(%rbp)
0x100003950 <+16>: movq %rsi, -0x60(%rbp)
-> 0x100003954 <+20>: callq 0x100003ba0 ; Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at main.swift:95
0x100003959 <+25>: movq 0x6a8(%rip), %rcx ; (void *)0x00007fff80cc5020: type metadata for Any
0x100003960 <+32>: addq $0x8, %rcx
0x100003967 <+39>: movq %rax, 0x4702(%rip) ; Swift11_闭包.fn : (Swift.Int) -> Swift.Int
0x10000396e <+46>: movq %rdx, 0x4703(%rip) ; Swift11_闭包.fn : (Swift.Int) -> Swift.Int + 8
0x100003975 <+53>: movl $0x1, %edi
0x10000397a <+58>: movq %rcx, %rsi
- LLDB输入指令si,进入
getFn
函数的汇编代码实现:
Swift11_闭包`getFn():
-> 0x100003ba0 <+0>: pushq %rbp
0x100003ba1 <+1>: movq %rsp, %rbp
0x100003ba4 <+4>: subq $0x20, %rsp
0x100003ba8 <+8>: movq $0x0, -0x8(%rbp)
0x100003bb0 <+16>: leaq 0x499(%rip), %rdi
0x100003bb7 <+23>: movl $0x18, %esi
0x100003bbc <+28>: movl $0x7, %edx
0x100003bc1 <+33>: callq 0x100003e96 ; symbol stub for: swift_allocObject
0x100003bc6 <+38>: movq %rax, %rcx
0x100003bc9 <+41>: addq $0x10, %rcx
0x100003bcd <+45>: movq %rcx, -0x8(%rbp)
0x100003bd1 <+49>: movq $0x0, 0x10(%rax)
0x100003bd9 <+57>: movq %rax, %rdi
0x100003bdc <+60>: movq %rax, -0x10(%rbp)
0x100003be0 <+64>: callq 0x100003ec0 ; symbol stub for: swift_retain
0x100003be5 <+69>: movq -0x10(%rbp), %rdi
0x100003be9 <+73>: movq %rax, -0x18(%rbp)
0x100003bed <+77>: callq 0x100003eba ; symbol stub for: swift_release
0x100003bf2 <+82>: leaq 0x277(%rip), %rax ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
0x100003bf9 <+89>: movq -0x10(%rbp), %rdx
0x100003bfd <+93>: addq $0x20, %rsp
0x100003c01 <+97>: popq %rbp
0x100003c02 <+98>: retq
-
callq 0x100003e96
:调用函数,从右侧注释可知,是在堆区分配空间,即创建了实例对象,然后将实例对象的内存地址返回,存放到rax寄存器中; - LLDB中输入
register read rax
,得到rax = 0x0000000103023160,即堆空间的地址值; -
movq %rax, -0x10(%rbp)
与movq -0x10(%rbp), %rdx
,先将(rbp-0x10)=rax,然后(rbp-0x10)=rdx,即rdx=rax,也就是rdx寄存器中也存放着堆空间的地址值; -
leaq 0x277(%rip), %rax
:将(rip+ 0x277)地址写入rax寄存器,由右侧的注释可知(rip+ 0x277)地址是与plus函数相关的内存地址; - 当汇编代码停在下面的代码行,LLDB调试如下:
- 最终
rax = plus函数相关的内存地址
,rdx = 堆空间的内存地址
- 过掉断点,进入到
plus
函数内部,如下所示:
- 然后回到main函数,如下所示:
- 将plus函数地址与堆内存地址分别赋值给全局变量fn的16个字节;
- 上面是创建闭包变量的汇编逻辑,下面来阐述闭包调用函数的逻辑;
-
fn(1)
是调用plus函数,而plus函数是存储在变量fn的前8个字节,也就是存储在rax寄存器中的,所以fn(1)
对应的汇编代码如下:
-
fn(1)
-->call *%rax
-
fn(1)
,参数1,是怎么传的呢,如下: -
movl $0x1, %edi
,将参数1,写入edi寄存器,也就是写入rdi寄存器; -
fn(1)
本质是调用plus函数,其内部会访问堆空间中的num,所以plus函数必定会访问堆空间,那么其是怎么访问的呢?如下: - 首先num在堆空间的内存布局如下:
-
movq 0x438c(%rip), %rcx
,由右侧注释可知rcx=fn+8,也就是堆内存地址; movq %rcx, -0x60(%rbp)
-
movq -0x60(%rbp), %r13
:r13 = 堆内存地址; - 最终:rdi = 1,r13 = 堆内存地址,由于以下报错,不能进入plus函数内部的执行逻辑,中断推理;
- 将上述代码再做修改,如下:
- 将局部变量改成全局变量,LLDB输入指令si,进入
getFn
函数的汇编如下所示:
- 看到没有创建堆空间对象,也就是说没有捕获全局变量,这是因为全局变量作用域广,不会被释放,而局部变量出了作用域就会被释放,为了保住局部变量不被释放,所以将其捕获至堆空间;
- 看下面的代码,如下所示:
- 汇编代码如下:
- 看到两个
swift_allocObject
函数,说明创建了两个堆内存空间对象,分别捕获num1,num2,是两块独立的堆内存; - 将断点停在
m(5)
所在行,且汇编代码中的断点,加在第一个swift_allocObject
函数下面,如下所示:
-
rax = 0x0000000100709870
是num1的堆内存空间; - 然后过掉断点,来到
m(5)
所在行的断点,然后查看堆内存空间0x0000000100709870,如下所示:
- 然后将汇编代码中的断点,加在第二个
swift_allocObject
函数下面,重新运行,如下所示:
-
rax = 0x000000010073adf0
是num2的堆内存空间; - 然后过掉断点,来到
m(5)
所在行的断点,然后查看堆内存空间0x000000010073adf0,如下所示:
自动闭包
//若第一个数>0,就返回第一个数,否则返回第二个数
func getFirstPositive1(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
getFirstPositive1(10, 20) //10
getFirstPositive1(-2, 20) //20
getFirstPositive1(0, -4) //-4
- 将上述代码进行改写,如下:
//将第二个参数改成 无参返回Int的函数类型 可以让v2延迟执行
func getFirstPositive2(_ v1: Int,_ v2: () -> Int) -> Int? {
return v1 > 0 ? v1 : v2()
}
getFirstPositive2(10, {20})
getFirstPositive2(10) {20}
- 将第二个参数改成 无参返回Int的函数类型,那么传参时,传入的是函数闭包;
-
getFirstPositive2(10, {20})
{ 20 }是闭包函数; -
getFirstPositive2(10) {20}
是尾随闭包
的写法; - v2这个闭包函数,只有当v1<0的时候才会调用,也就是所谓的v2闭包函数可能会延迟执行;
- 使用自动闭包的关键字
autoclosure
,改写如下:
func getFirstPositive3(_ v1: Int,_ v2: @autoclosure () -> Int) -> Int? {
return v1 > 0 ? v1 : v2()
}
getFirstPositive3(10, 20)
- 与上面的区别在于,由于加了自动闭包的关键字
autoclosure
,所以在函数调用,传参时会自动加上闭包的大括号,所以可简写成getFirstPositive3(10, 20)
,虽然闭包参数传入的是20,其本质还是传入{ 20 }
- @autoclosure 会自动将20 封装成闭包{ 20 }
- @autoclosure 只支持 () -> T 类型的参数;
- 空合并运算符 ?? 使用了@autoclosure技术;
- 有@autoclosure与无@autoclosure 的函数,构成了函数重载;