Swift -- 05 闭包

577 阅读7分钟

swift.webp

闭包表达式

在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来隔离参数、返回值和函数体代码;

iShot_2022-04-30_19.13.00.jpg

闭包表达式的调用:
调用闭包表达式,可以省略参数标签;如上闭包,直接调用: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 分别代表参数v1v2;
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}

闭包

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

  • 一般指定义在函数内部的函数;
  • 一般它捕获的是外层函数的局部变量、常量

iShot_2022-05-18_15.38.58.jpg

例子:

iShot_2022-05-18_15.43.13.jpg 问题:

为什么上面的代码执行没有问题?
按照正常来说, num是getFn()函数的局部变量,当248行代码调用完成后,
getFn()这句代码已经执行结束,局部变量将在结束后销毁;

那么为什么249行及之后的代码,调用没有问题,并且plus函数也能使用num变量?

并且每次print调用fn时,访问的num都是同一块内存?

通过汇编解答 1、首先,我们在plus函数内,直接return i,不调用num,查看汇编源码

iShot_2022-05-18_16.06.43.jpg

2、我们在plus函数内,调用numreturn num,查看汇编源码

iShot_2022-05-18_17.34.29.jpg

汇编

通过汇编分析闭包业务

  • 代码
//定一个Fn,Fn是一个函数类型,接收Int类型参数,返回一个Int类型值
typealias Fn = (Int)->Int
//定义 getFn函数, 返回值为:函数
func getFn()->Fn
{
    //num 局部变量
    var num = 0
    func plus(_ iInt)->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的地方打断点,查看汇编;

iShot_2022-05-19_16.38.32.jpg

汇编分析 iShot_2022-05-19_17.21.41.jpg

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函数内部

iShot_2022-05-19_17.30.03.jpg

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,查看汇编;

iShot_2022-05-19_17.54.34.jpg

iShot_2022-05-19_17.56.24.jpg

从之前的分析,我们可以得知,getFn函数返回的分别是rax 和 rdx
结合经验,这次我们也主要分析,捕获num的情况,rax 和 rdx分别是什么?

3、将num放到全局区

iShot_2022-05-26_15.32.05.jpg

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,构成了函数重载