一、闭包表达式(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))
/*输出结果*/
11
13
问题一
- 这里我们注意到一个问题,当
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
image1.png 可以看到
getFn()之后,有两次movq,其中q代表8个字节。跟上一个的断点调试比较。我们知道%rax里面存放的是plus,%rdx里面存放的是num的堆地址。结合本次的断点,我们可以得出结论,fn1里面存放的就是plus(前8个字节)和num(后8个字节)的地址。-
注意:
%rax里面存放的并不是plus的实际地址值,而是一个简洁的地址值。可以理解为在plus地址上面加了一层包装。我们可以同时在plus函数内部打上断点。先读取%rax,然后再进入plus函数内部来比较一下,我们可以发现。
image.png 通过上文我们知道
fn1的前8个字节存放的是plus经过包装后的地址值,那么执行汇编指令的时候,就要从前8个字节中取出地址,在执行call指令。这种情况下,编译器第一时间并不知道plus的地址,因此汇编代码callq 0x100003d70这种有固定地址的。而是这种callq *%rax(寄存器里面存储的东西是变化的)。-
接下来我们找到
callq *%rax,并跟进去,会发里面会有jmp指令,跳转到plus函数里面
image.png


- 这里补充一点关于寄存器的知识:
① 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”






