一、闭包表达式(Closure Expression)
- 在Swift中,可以通过
func
定义一个函数,也可以通过闭包表达式
定义一个函数
///函数
func sum(_ v1: Int, _ v2: Int) -> Int {v1 + v2}
///闭包表达式
var fn = {
(v1: Int, v2: Int) -> Int in
return v1 + v2
}
fn(10,20)
///或者这样
{
(v1: Int, v2: Int) -> Int in
return v1 + v2
}(10,20)
闭包表达式的格式
{
(参数列表) -> 返回值类型 in
函数体代码
}
闭包表达式的简写
func exec(v1: Int, v2: Int, fn: (Int, Int) -> Int) {
print(fn(v1, v2))
}
///1、正常写法
exec(v1: 10, v2: 20) { (v1: Int, v2: Int) -> Int in
return v1 + v2
}
///2、省略参数类型
exec(v1: 10, v2: 20) {
v1, v2 in return v1 + v2
}
///3、省略return
exec(v1: 10, v2: 20) {
v1, v2 in v1 + v2
}
///4、用美元符表示
exec(v1: 10, v2: 20) { $0 + $1 }
///5、最简单
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(v1, v2))
}
exec(fn: { $0 + $1 })
exec() { $0 + $1 }
exec { $0 + $1 }
示例:数组的排序
@inlinable public mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
根据函数定义,我们只需要传入一个函数即可(排序规则)
/// 返回true: i1排在i2前面
/// 返回false: i1排在i2后面
func cmp(i1: Int, i2: Int) -> Bool {
// 大的排在前面
return i1 > i2
}
var nums = [20, 1, 60, 25, 95, 8, 5]
nums.sort(by: cmp)
当然,我们还可以使用闭包表达式
来定义排序规则
nums.sort(by: {
(i1: Int, i2: Int) -> Bool 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 }
忽略参数
- 当
返回值
与传入的参数
没有关系的时候,我们可以忽略参数
func exec(fn: (Int, Int) -> Int) {
print(fn(10,20))
}
exec { _,_ in 30}
二、闭包(Closure)
什么是闭包?
- 一个
函数
和它所捕获的变量\常量
环境组合起来,称为闭包
① 一般指:定义在函数内部的函数
② 一般它捕获的是外层函数的 局部 变量\常量
③ 闭包是引用类型
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 10
func plus(_ i: Int) -> Int {
num += i
return num
}
return plus
} /// 返回的plus和num形成了闭包
var fn1 = getFn()
print(fn1(1))
print(fn1(2))
/*输出结果*/
1
3
问题一
- 这里我们注意到一个问题,当
getFn()
执行完毕之后,局部变量num
会被销毁,那么plus
函数又是怎么捕获的呢?
下面我们通过窥探汇编代码来看一下:我们将断点打在return plus
位置
image.png - 通过上图我们可以看到,在
return plus
之前,编译器申请了一段堆空间的内存,在这里我们可以初步猜想num
就是被存在这一个新分配的堆空间里面
- 下面我们继续断点调试(注意上图中
num
的初始值为0
,下面我们将num
的初始值设置为10
,这样方便观察)
image.png - 通过上图我们可以看到,局部变量
num
确实是被捕获之后,放到了新分配的堆空间地址
里面。
问题二
- 局部变量
num
是在什么时候被捕获的呢?
下面我们做一个简单的实验
typealias Fn = (Int) -> Int
func getFn() -> Fn {
var num = 10
func plus(_ i: Int) -> Int {
num += i
return num
}
num = 20
return plus
} /// 返回的plus和num形成了闭包
var fn1 = getFn()
print(fn1(1))
/*输出结果*/
21
- 可以看到,
num
的捕获是在return plus
之前捕获,所以在return plus
之前,num
值的改变,并不影响捕获。 - 注意:如果
num
是全局变量,并不会发出捕获的行为,因为全局变量并不会随着函数的销毁而销毁。(函数调用完,内存会被回收,生命周期结束)
总结:一个
函数
和它所捕获的变量\常量
环境组合起来,称为闭包
① 一般指:定义在函数内部的函数
② 一般它捕获的是外层函数的 局部 变量\常量
我们也可以把闭包
想象成一个类的实例对象
① 内存在堆空间
② 捕获的局部变量\常量就是对象的成员(存储属性)
③ 组成的闭包
的函数就是类内部定义的方法
问题三
-
fn1
里面存储存放的是什么?(猜想:fn1
里面存放的是num
的堆空间地址值 和plus
的地址值)(注意:print(MemoryLayout.size(ofValue: fn1))
的输出结果为16
) -
%rax
和%rdx
常作为函数的返回值使用 -
lea
指令 load effective address, 加载有效地址,可以将有效地址传送到指定的的寄存器。指令形式是从存储器读数据到寄存器, 效果是将存储器的有效地址写入到目的操作数, 简单说, 就是C语言中的”&”.
下面我们通过断点调试来看一下:
1、首先我们再return plus
处打一个断点,窥探一下汇编代码。
image.png
-
下面我们换个地方打断点
image.png
image1.png -
可以看到
getFn()
之后,有两次movq
,其中q
代表8个字节
。跟上一个的断点调试比较。我们知道%rax
里面存放的是plus
,%rdx
里面存放的是num
的堆地址。结合本次的断点,我们可以得出结论,fn1
里面存放的就是plus
(前8个字节)和num
(后8个字节)的地址。 -
注意:
image.png%rax
里面存放的并不是plus
的实际地址值,而是一个简洁的地址值。可以理解为在plus
地址上面加了一层包装。我们可以同时在plus
函数内部打上断点。先读取%rax
,然后再进入plus
函数内部来比较一下,我们可以发现。
-
通过上文我们知道
fn1
的前8个字节存放的是plus
经过包装后的地址值,那么执行汇编指令的时候,就要从前8个字节中取出地址,在执行call
指令。这种情况下,编译器第一时间并不知道plus
的地址,因此汇编代码callq 0x100003d70
这种有固定地址的。而是这种callq *%rax
(寄存器里面存储的东西是变化的)。 -
接下来我们找到
image.pngcallq *%rax
,并跟进去,会发里面会有jmp
指令,跳转到plus
函数里面
- 这里补充一点关于寄存器的知识:
① rax、rdx 常作为函数返回值使用
② rdi、rsi、rdx、rcx、r8、r9 等寄存器常用于存放函数参数
③ rsp、rbp 用于栈操作
④ rip 作为指令指针,1、存储着CPU下一条要执行的指令地址,2、一旦CPU读取一条指令,rip会自动指向下一条指令(存储下一条指令的地址)
问题四
-
num
和i
是怎么传入plus
函数里面的呢?
我们继续通过汇编来查看一下
image.png
三、自动闭包(@autoclosure)
我们首先来看一下@autoclosure
的使用
我们先定义一个函数,用来获取第一个正数
func getFirstPositive(_ v1: Int, _ v2: Int) -> Int {
return v1 > 0 ? v1 : v2
}
print(getFirstPositive(10, 30))
print(getFirstPositive(-1, 2))
/*输出结果*/
10
2
这里我们可以将v2
改成一个函数,如下:
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
getFirstPositive(10) { 20 }
- 当然,如果函数体很长的话,这样写没什么问题,但是如果像我们上面写的那样,函数体很短,则可读性和美观性都不足。
- 这个时候我们就可以使用
@autoclosure
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
return v1 > 0 ? v1 : v2()
}
getFirstPositive(10, 20)
-
@autoclosure
会自动将20
封装成闭包{ 20 }
-
@autoclosure
只支持() -> T
格式的参数 -
@autoclosure
并非只支持最后一个参数 - 空合并运算符
??
使用了@autoclosure
的技术(public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
) - 有
@autoclosure
、无@autoclosure
,构成了函数重载
func getFirstPositive(_ v1: Int, _ v2: () -> Int) -> Int {
print("无 @autoclosure")
return v1 > 0 ? v1 : v2()
}
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
print("有 @autoclosure")
return v1 > 0 ? v1 : v2()
}
print(getFirstPositive(10, 20))
print(getFirstPositive(45, {30}))
/*输出结果*/
有 @autoclosure
10
无 @autoclosure
45
注意:为了避免与期望冲突,使用了@autoclosure
的地方最好注明清楚:这个值会被延迟执行
- 下面我们来看一下为什么要注意这一点:
func fn() -> Int {
print("延迟执行")
return 20
}
func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int {
print("有 @autoclosure")
return v1 > 0 ? v1 : v2()
}
print(getFirstPositive(10, fn()))
/*输出结果*/
有 @autoclosure
10
我们会发现,因为v1
大于0,所以fn()
并没有执行。
正常来讲fn()
代表函数的执行,但是这里并没有执行,所以这一点要注意。
四、逃逸闭包
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注@escaping
,用来指明这个闭包是允许逃逸
出这个函数的。
func fn() -> Int {
return 20
}
func getFirstPositive(_ v1: Int, _ v2: @escaping ()->Int) -> ()->Int {
return v2
}
var fn1 = getFirstPositive(10, fn)
- 一种能使闭包
逃逸
出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为completion handler
。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束之后才会被调用。在这种情况下,闭包需要逃逸
出函数,因为闭包需要在函数返回之后被调用。例如:
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
someFunctionWithEscapingClosure(_:)
函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为@escaping
,就会得到一个编译错误。
将一个闭包标记为@escaping
意味着你必须在闭包中显式的引用self
。比如说,在下面的代码中,传递到someFunctionWithEscapingClosure(_:)
中的闭包是一个逃逸闭包,这意味着它需要显式的引用self
。相对的,传递到someFunctionWithNonescapingClosure(_:)
中的闭包是一个非逃逸闭包,这意味着它可以隐式引用self
。
var compltionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
compltionHandlers.append(completionHandler)
}
func someFunctionWithNonecapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure {
self.x = 100
}
someFunctionWithNonecapingClosure {
x = 200
}
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出“200”
compltionHandlers.first?()
print(instance.x)
// 打印出“100”
网友评论