闭包表达式
在Swift中,可以使用func
定义一个函数,也可以通过闭包表达式
定义函数;
例如:
通过func
定义函数:
func sum(_ x:Int,_ y:Int) ->Int {
return x+y
}
sum(x:10,y:20)
通过闭包表达式
定义函数:
var fn = {
(x:Int,y:Int)->Int in
return x + y
}
fn(10,20)
闭包表达式的规范:
使用in
来隔离参数、返回值和函数体代码;
闭包表达式的调用:
调用闭包表达式,可以省略参数标签;如上闭包,直接调用:fn(10,20);
不需要像func
函数那样,需要使用参数标签:sum(x:10,y:20);
闭包表达式的简写
func 函数:
func sum(v1:Int,v2:Int fn:(Int,Int)->Int)
{
print(fn(v1,v2))
}
闭包表达式写法:
//常规写法
sum(v1:10,v2:20,fn:{
(v1:Int,v2:Int)->Int in return v1 + v2
})
//简写:省略闭包的参数类型和返回值类型
因为sum函数的参数v1,v2已经明确声明为 Int 类型,因此系统会自动识别类型
sum(v1:10,v2:20,fn:{
v1,v2 in return v1 + v2
})
return 也可以省略:
sum(v1:10,v2:20,fn:{
v1,v2 in v1 + v2
})
使用$来代表参数列表:$0 、 $1 分别代表参数v1、v2;
in 也可以省略,之前使用in,是用来区别函数体和参数别表的,既然不存在参数列表了,那么in就可以省略:
sum(v1:10,v2:20,fn:{
$0 + $1
})
更变态的写法:
sum(v1:10,v2:20,fn:+)
不太建议使用后两种写法,可能会被打。太简洁了,可能会造成阅读障碍 😂
尾随闭包
如果将一个很长的闭包表达式作为函数最后一个实参
,使用尾随闭包
可以增强函数的可读性
;\
尾随闭包写法:
尾随闭包是一个可以写在函数括号外面(后面)的闭包表达式;
例如:
sum(v1:10,v2:20) {
(v1:Int,v2:Int)->Int in return v1 + v2
}
如果闭包表达式是函数唯一实参,而且使用了尾随闭包的写法,那么可以省略函数名后面的扣号:
例如:
func ex(fn:(Int,Int)->Int){
print(fn(1,2))
}
调用写法:
ex(){$0 + $1} 或者
ex{$0 + $1}
数组排序
func testArraySort(){
var array = [1,15,2,40,10]
//方式一:函数式自定义排序
array.sort(by: cmp(v1:v2:))
//方式二:闭包式自定义排序
array.sort(){(v1,v2) in v1 > v2}
print(array)
}
//函数式自定义排序
func cmp(v1:Int,v2:Int)->Bool{
return v1 > v2
}
//调用
testArraySort()
忽略参数
在Swift中,如果想忽略某个参数,可以使用_
表示;
例子:
func ex(fn:(Int,Int)->Int){
print(fn(1,2))
}
ex { _, _ in 10}
闭包
闭包:一个函数
和它所捕获的变量 或 常量
环境组合起来,称为闭包\
- 一般指定义在函数内部的函数;
- 一般它捕获的是外层函数的局部变量、常量
例子:
问题:
为什么上面的代码执行没有问题?
按照正常来说, num是getFn()函数的局部变量,当248行代码调用完成后,
getFn()这句代码已经执行结束,局部变量将在结束后销毁;
那么为什么249行及之后的代码,调用没有问题,并且plus函数也能使用num变量?
并且每次print调用fn时,访问的num都是同一块内存?
通过汇编解答
1、首先,我们在plus
函数内,直接return i
,不调用num
,查看汇编源码
2、我们在plus
函数内,调用num
,return num
,查看汇编源码
汇编
通过汇编分析闭包业务
- 代码
//定一个Fn,Fn是一个函数类型,接收Int类型参数,返回一个Int类型值
typealias Fn = (Int)->Int
//定义 getFn函数, 返回值为:函数
func getFn()->Fn
{
//num 局部变量
var num = 0
func plus(_ i: Int)->Int
{
num += i
return num
}
return plus(_:)
}
//调用
var fn1 = getFn()
fn1(1)
- 查看
fn1
占用字节大小
//通过MemoryLayout可以查看fn1占用字节大小
print(MemoryLayout.stride(ofValue: fn1))
//占用大小为:16字节
分析fn1
这个变量里,都有什么?
1、未捕获num
的情况
plus
函数直接return i
,并在初始化fn1
的地方打断点,查看汇编;
汇编分析
1、
callq 代表函数调用,此处表示调用getFn函数,后面的地址就是getFn函数的地址
2、
函数调用完毕,它的返回值存在rax寄存器中;
根据经验,可以看出 0x8b0a 是全局区地址
movq %rax, 0x8b0a(%rip) ,表示:将rax 赋值到 0x8b0a(%rip)这个地址里;
movq %rdx, 0x8b0b(%rip),也表示:将rdx 赋值到 0x8b0b(%rip)这个地址里;
3、
函数返回值所占大小为8字节,但fn1这个变量占用长度为16字节;那么getFn函数是如何返回16字节的呢?
4、知识补充
movq,长度为q,表示8字节大小;
getFn
函数汇编
在控制台输入:si
,然后回车,进入getFn
函数内部
1、leaq 0xd(%rip), %rax ; plus(Swift.Int) -> Swift.Int at main.swift:243
从苹果给的提示可以看到,这句汇编是:
将plus函数的地址值(0xd(%rip)交给rax
2、xorl %ecx, %ecx
xorl表示异或,此处异或两个相同的值,结果为0,并且把异或结果交给ecx
3、movl %ecx, %edx
将 ecx 赋值给 edx,也就是将 0 赋值给 edx;
edx 是rdx的一部分,所以也就意味着:将 0 赋值给 rdx;
4、总结
rax 存放 plus函数的地址值;
rdx 存放 0;
getFn
函数汇编总结
movq %rax, 0x8b0a(%rip) ; QLYTestSwift.fn1 : (Swift.Int) -> Swift.Int
movq %rdx, 0x8b0b(%rip) ; QLYTestSwift.fn1 : (Swift.Int) -> Swift.Int + 8
fn1 前 8个字节,存放plus函数地址;
fn1 后 8哥字节,存放 0;
2、捕获num
的情况
plus
函数直接return num
,查看汇编;
从之前的分析,我们可以得知,getFn
函数返回的分别是rax 和 rdx
;
结合经验,这次我们也主要分析,捕获num
的情况,rax 和 rdx
分别是什么?
3、将num
放到全局区
将num
放到全局区,getFn
的汇编代码明显比num 作为局部变量
时少很多;
并且没有产生堆空间内存,也就是没有对num
进行捕获;
num
放在全局区
为什么没有对num
进行捕获呢?
总结:
1、捕获num
的情况,系统会调用swift_allocObject
,给num
分配堆空间内存;
2、全局区num
不分配内存空间,num
存放在代码段;
3、num 作为局部变量
需要产生堆空间内存,主要是为了保num
的命;
从生命周期来说,var fn1 = getFn()
这句代码调用结束后,getFn的栈空间就没了,局部变量num
就会随之函数调用结束而销毁,所以为了让num
继续存留,需要把num
放到堆空间上;
4、如果把num
作为全局变量,num
就会存在全局区数据段内,num
将持续保活;
自动闭包 @autoclosure
例子:
func getNumber() -> Int
{
let a = 10
let b = 20
return a+b
}
//v1、v2是省略参数标签 的参数;
//函数 返回 int 类型
func getFirstPositive(_ v1:Int, _ v2:Int) ->Int
{
//如果第一个数大于0,返回第一个数,否则返回第二个数
return v1 > 0 ? v1:v2
}
getFirstPositive(10, getNumber())
从代码上看,10大于0,可以返回10。但是getNumber()还是依然会调用
那么在这种情况下,如何减少getNumber()调用,减少代码的浪费执行
改良版:
v2返回值为:函数,一个不接收任何参数,返回Int类型的函数;
通过调用函数,得到v2返回的整数值;
如此,如果v1大于0,那么直接就返回v1,v2函数就不会再调用;
相反,如果v1小于0,再调用v2函数,返回 v2函数的返回值;
func getFirstPositive(_ v1:Int, _ v2:()->Int) ->Int
{
return v1 > 0 ? v1:v2()
}
getFirstPositive(10,{
let a = 1
let b = 2
print("test")
return a+b
})
或者使用尾随闭包
getFirstPositive(10){
let a = 1
let b = 2
print("test")
return a+b
}
对于代码量大的时候,使用闭包的方式非常方便;
但代码量少的时候,使用闭包,代码将有一丢丢的不太美观,可读性就比较差;
例如:
getFirstPositive(10),{20})
或者:
getFirstPositive(10){
return 10
}
这时候,@autoclosure(自动闭包)的作用就体现了:
func getFirstPositive(_ v1:Int, _ v2: @autoclosure ()->Int) ->Int
{
//如果第一个数大于0,返回第一个数,否则返回第二个数
return v1 > 0 ? v1:v2()
}
getFirstPositive(10,20)
传递整数 20,@autoclosure 自动将20,生成闭包表达式 -----> {20};
注意事项:
1、@autoclosure只支持 () -> T 格式的参数,必须是无参的,并且有返回值的;
2、@autoclosure 并非只支持最后一个参数;
3、空合并运算符?? 使用的也是@autoclosure技术
4、有@autoclosure、无@autoclosure,构成了函数重载