Swift进阶(五)闭包与闭包表达式

一、闭包表达式(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

image.png

  • 下面我们换个地方打断点


    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

image1.png
image2.png
  • 这里补充一点关于寄存器的知识:
    ① rax、rdx 常作为函数返回值使用
    ② rdi、rsi、rdx、rcx、r8、r9 等寄存器常用于存放函数参数
    ③ rsp、rbp 用于栈操作
    ④ rip 作为指令指针,1、存储着CPU下一条要执行的指令地址,2、一旦CPU读取一条指令,rip会自动指向下一条指令(存储下一条指令的地址)

问题四

  • numi是怎么传入plus函数里面的呢?
    我们继续通过汇编来查看一下
    image.png
image1.png
image2.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 ?? (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”

你可能感兴趣的:(Swift进阶(五)闭包与闭包表达式)