Swift 闭包

Swift 闭包

[TOC]

前言

我个人觉得在看这篇文章前,先了解一下Swift 函数,会有助于您更加深刻的理解闭包。

闭包是一个捕获了上下文的常量或者变量的函数,也可以说是字包含的函数代码块,可以在代码中被传递和使用。Swift中的闭包与CObjective-C中的代码块blocks以及其他一些编程语言中的匿名函数Lambdas比较相似。

闭包可以捕获和存储其所在上下文中任意常量和变量的引用。被称为包裹常量和变量。并且Swift会为你管理在捕获过程中涉及到的所有内存操作。

1. 闭包

1.1 闭包简介

「全局函数」是一个有名字但不会捕获任何值的闭包

func test(){
    print("test")
}

「嵌套函数」是一个有名字并可以捕获其封闭函数内值的闭包。

func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

「闭包表达式」使我们最常用的闭包的一种,是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包。

{ (parameters) -> return type in
    statements
}

使用闭包有很多好处:

  1. 可以利用上下文自动推断参数和返回值的类型
  2. 隐士返回单表达式闭包,也就是但表达式的时候可以省略return关键字
  3. 参数名称可以简写或者不写,使用$0表示第一个参数
  4. 尾随闭包表达式

1.2 闭包表达式

闭包表达式是一种构建内联闭包的方式,这个表达式需要:

  1. 通过大括号{}来确定作用域
  2. 可以有参数和返回值
  3. 如果有参数的时候需要通过in关键字来表明其后面的代码是其函数体

Swift标准库提供了名为 sorted(by:)的方法,它会基于你提供的排序闭包表达式的判断结果对数组中类型确定的值进行排序。一旦它完成排序过程,sorted(by:)方法或返回一个与旧数组类型大小相同类型的新数组,该数组的元素有着正确的排序顺序。原数组不会被sorted(by:)方法修改。

@inlinable public func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element]

下面我们就通过对数组内的内容进行排序,来体验一下闭包表达式的魅力。

首先定义一个数组:

var array = [1, 2, 3]
  • 完整写法
// 完整写法
let newArray = array.sorted(by: { (item1: Int, item2: Int) -> Bool in
    return item1 < item2
})
  • 因为此闭包函数体部分很短,我们可以将其改写成一行代码
// 简短闭包函数体单行写法
let newArray = array.sorted(by: { (item1: Int, item2: Int) -> Bool in return item1 < item2 })
  • 根据上下文推断类型

因为这个排序的闭包表达式是作为sorted(by:)方法的参数传入的,Swift可以推断其参数和返回值类型,因此其参数必须是(Int, Int) -> Bool类型的函数,也就意味值(Int, Int)Bool类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略。

// 根据上下文推断类型
let newArray = array.sorted(by: { item1, item2 in return item1 < item2 })

实际上,通过内联闭包表达式构造的闭包作为参数时,总能够推断出参数和返回值的类型,所以此时你几乎不需要利用完整格式构造内联闭包。尽管如此,如果完整的格式的闭包能够提高代码的可读性,则尽量书写完整,在此处看见sorted就能够知道是排序,所以对于读者来说能够推测出这个闭包是处理数组排序的。

  • 但表达式闭包的隐士返回

Swift支持单行表达式省略return关键字来隐式的返回表达式的结果,所以在闭包中我们可以写成这样:

// 单表达式闭包的隐式返回
let newArray = array.sorted(by: { item1, item2 in item1 < item2 })
  • 参数名称缩写

Swift 自动为内联闭包提供了参数名称的缩写功能,我们可以通过$0,$1,$2.....来顺序的调用闭包的参数。如果在闭包中省略了参数列表,那么in关键字也随之可以省略,此时闭包表达式完全由闭包函数体构成:

// 参数名称缩写
let newArray = array.sorted(by: { $0 < $1 })
  • 运算符方法

实际上还有一种更简短的方式来编写上面例子中的闭包表达式。Swift的Int类型定义了关于小于号(<)的字符串实现,其作为一个函数接受两次Int类型的参数并返回Bool类型的值。而这正好与sorted(by:)方法的参数需要的函数类型相符合。因此我们可以简单的传递一个小于号,Swift可以自动推断找到系统自带的那个Int函数的实现:

// 运算符方法
let newArray = array.sorted(by: <)
  • 尾随闭包

当然,sorted(by:)方法只有一个参数,也就是最后一个参数是闭包,我们还可以使用尾随闭包的写法:

let newArray1 = array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
let newArray2 = array.sorted { (item1, item2) in return item1 < item2}
let newArray3 = array.sorted { (item1, item2) in item1 < item2}
let newArray4 = array.sorted { $0 < $1}

1.3 闭包的应用

闭包相当于一种类型,可以作为常量/变量,所以也就可以作为参数进行传递。

变量闭包:

var closure: (Int) -> Int = { (a: Int) in
    return a
}

同样我们也可以将闭包为可选类型:

var closure: ((Int) -> Int)?
closure = nil

我们也可以声明一个常量的闭包,这里就是赋值后就不能改变了

let clourse: (Int) -> Int
clourse = {(age: Int) in
    return age
}
// 如果在赋值就会报错:
// Immutable value 'clourse' may only be initialized once
clourse = {(age: Int) in
    return age
}

同时也可以作为函数的参数:

func test(param: () -> Int){
    print(param())
}

var closure: () -> Int = { () -> Int in
    return 10
}

test(param: closure)

尾随闭包:

如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数的时候,将这个闭包替换成尾随闭包的形式能够提高代码的可读性。

尾随闭包就是在函数小括号()之后的闭包表达式,此时不用写出它的参数标签。

//闭包表达式作为函数的最后一个参数
func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item2: Int, _ item3: Int) -> Bool) -> Bool{
    return by(a, b, c)
    
}
// 以下是不使用尾随闭包进行函数调用
test(10, 20, 30, by: {(_ item1: Int, _ item2: Int, _ item3: Int) -> Bool in
        return (item1 + item2 < item3)
})
// 以下是使用尾随闭包进行函数调用
test(10, 20, 30) { (item1, item2, item3) -> Bool in
    return (item1 + item2 < item3)
}

2. 值捕获

首先说明一下,摘自维基百科闭包 (计算机科学),闭包在实现上一个结构体,它存储了一个函数(通常是其入口地址)和一个关联环境(相当于一个符号查找表)......

闭包可以在其被定义的上下文中捕获常量或变量,即使定义这些常量和变量的员作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

Swift中,可以捕获值的闭包的最简单形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

看看下面这段代码的打印结果:

func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
let makeInc = makeIncrementer()
print(makeInc())
print(makeInc())
print(makeInc())


11
12
13

从打印结果中我们可以看到,每次的结果都是在上次执行的基础上变化的。其实给我们的第一印象是runningTotal是个临时变量,每次进入函数都是10。这里能够累加的原因是什么呢?

如果我们这样写:

print(makeIncrementer()())
print(makeIncrementer()())
print(makeIncrementer()())


11
11
11

此时我们可以看到打印结果都是11了,对此我们能够理解的是:

  • 第一种方式,函数已经确定,runningTotal被捕获了,每次调用的时候访问的应该不是那个临时变量了。
  • 第二种方式,每次都重新创建的一个函数,每次调用都是新的

那么这些原理是什么呢?下面我们就来探索一下:

2.1 通过SIL代码分析

将上述代码转换为sil代码:(转换方法可参考我以前的文章)

sil代码中我们主要看makeIncrementer函数内部的代码:

image
  1. 首先通过alloc_box申请了堆区内存,并把该内存的引用给了runningTotal这边变量名称
  2. 然后通过project_box取出变量地址,将变量值存储到里面
  3. 在闭包进行调用的时候是将这个引用传入到闭包中
  4. 在调用前后还对这个存储进行了strong_retainstrong_release的操作

所以捕获值的本质就是将变量存储到了堆山,然后通过对齐的引用进行使用。

2.2汇编代码在看一看

image

在汇编代码中我们也可以看到调用了swift_allocObject去开辟内存,同样也通过strong_retainstrong_release对开辟的内存进行引用计数的处理。

2.3 闭包是引用类型

在上面的例子中,尽管makeInc是由let修饰的常量,但我们多次调用makeInc()仍然可以增加打印的值,这是因为函数和闭包都是引用类型

3. 通过IR代码探索闭包的本质

在上面的sil代码中其实我们并没有分析出过多的关于闭包的内容,所以为了更好的探索闭包的本质,我们通过IR代码对其进行进一步的分析。

Swift代码生成IR代码的命令如下:

swiftc -emit-ir 文件名 > ./main.ll

例如:
- cd 文件所在路径
- swiftc -emit-ir main.swift > ./main.ll

如果你想在生成后并将其打开则需要在后面添加&& open main.ll命令即可,前提是你安装了VSCode并指定VSCode是打开.ll文件的应用程序。

3.1 IR基本语法

首先我们来看看IR的基本语法

  • 数组:
// 1. elementnumber为数组中存储数据的个数
// 2. elementtype为数组中存储数据的类型
[ x ]



alloca [24 x i8], align 8 // 24个i8都是0
  • 结构体:
// %T - %是固定写法,T是取的名字
// type - 是关键字
//  - 是结构体内部属性列表
%T = type {} // 和C语言结构体类似


%swift.refcounted = type { %swift.type*, i64}

/*
这里就是:
- 名称为swift.refcounted的结构体
- 里面有一个swift.type的指针类型
- 还有一个i64,也就是64位整形 - 8字节
*/
  • 指针类型:
*



i64*
这里就是一个64位整形的指针
  • getelementptr指令:

在LLVM中我们需要使用getelementptr指令获取数组和结构体的成员,语法规则如下:

 = getelementptr , * {, [inrange]  }*

 = getelementptr inbounds , * {, [inrange]  }*

乍一看,很懵逼,下面我们看看LLVM官网中的例子:

struct munger_struct{
    int f1;
    int f2;
};

int munge(struct munger_struct *P){
    return P[0].f1 = P[1].f1 + P[2].f2;
}

struct munger_struct* array[3];

int main(int argc, const char * argv[]) {

    munge(array);
    return 0;
}

使用下面的例子将其转换为IR代码:

clang -S -emit-llvm main.c > ./main.ll && open main.ll
image

根据截图中的对比分析现在应该清楚一些了,下面我们在强化一下理解。

int main(int argc, const char * argv[]) {
    int array[4] = {1, 2, 3, 4};
    int a = array[0];
    return 0;
}
其中int a = array[0];这句对应的LLVM代码应该是这样的:

a = getelementptr inbounds [4 x i32], [4 x i32]* array, i64 0, i64 0
image
  • 第一个索引不会改变返回的指针的额类型,也就是说ptrval前面的*对应什么类型,返回就是什么类型
  • 第一个索引的偏移量是由第一个索引的值和第一个ty指定的基本类型共同确定的
  • 后面的索引是在数组或者结构体内进行索引
  • 每增加一个索引,就会使得该索引使用的基本类型和返回的指针类型去掉一层,例如[4 x i32]去掉一层就是i32

3.2 使用IR代码分析闭包的底层原理

下面我们回到makeIncrementer方法,并将其转换为IR代码:

Xnip2021-03-09_10-27-46

简述一下分析过程,其实看着很懵的,虽然分析了很多,但也没有特别理解。

  1. 首先看main函数,内部其实就是调用makeIncrementer方法
  2. 接着分析makeIncrementer方法
  3. 首先看返回值,返回的是一个结构体,内容为%6
  4. 那么就看%6,里面是传入的值
    1. 一个是结构体指针,内部存储的是一个符号对应的,也就是内部函数的指针
    2. 第二个存储的是%1,是swift.refcounted结构,实际是捕获的值
    3. swift.refcounted是一个结构体,定义在最上面,里面是i64*和i64两个值,也就是一个指针和一个整形,符合HeapObject结构
  5. 下面我们看%1里面存储了什么
    1. %1 里面是一个swift.refcounted*的指针
    2. 这个新的swift.refcounted结构的第一个成员存储的是通过Swift_allocObject初始化的HeapObject指针
    3. 这个HeapObject指针内存这一个对象结构,metadatarefcount等,可以从swift.full_boxmetadata看到,从0开始,到2结束
    4. 0就是metadata,1和2分别两个32位的refCount
    5. 接下来按位在%1后面初始化一个8字节的空间
    6. 将需要捕获的值的名称和值对应上,并将值存储到开辟的空间中
    7. 我觉得如果是对象的话这里存储的就是对象拷贝的指针地址,此处使用的是整形,所以指针存了值
  6. 最后我们也就基本知道了,返回的这个结构体,也就是Swift中返回的函数内部结构是什么了

下面我们就按照上面分析的结构,使用Swift代码仿写一下:

func makeIncrementer() -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}

// 函数对应的结构体
// 此处的泛型是第二个结构体成员指针对应的实际结构
struct FuntionData{
    // 函数地址
    var ptr: UnsafeRawPointer
    // 捕获值的指针
    var captureValue: UnsafePointer
}

// 捕获值的结构体
struct Box {
    // HeapObject
    var refCounted: HeapObject
    //捕获的值
    var value: T
}

// HeapObject
struct HeapObject{
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
    var f: () ->Int
}

let makeInc = makeIncrementer()
let f = VoidIntFun(f: makeInc)

// 初始化内存空间
let ptr = UnsafeMutablePointer.allocate(capacity: 1)
ptr.initialize(to: f)

// 内存重新绑定
let ctx = ptr.withMemoryRebound(to: FuntionData>.self, capacity: 1){$0.pointee}

print(ctx.ptr)
print(ctx.captureValue.pointee.value)

print("end")


0x0000000100002bd0
10

我们可以看到准确打印出了10,也就是捕获到的runningTotal的值。

下面我们看看打印的函数地址对应的符号,这里使用的是nm -p命令,打开终端,输入nm -p,将Mach-O文件拖拽到终端,后面拼接上| grep 打印的内存地址

示例:

nm -p /Users/sdyx/Library/Developer/Xcode/DerivedData/SwiftClosure-drehcgbowdolfibriszpnekjrboc/Build/Products/Debug/SwiftClosure | grep 0000000100002bd0

结果:

image

所以闭包,也可以说是函数:

  • 其本质就是一个结构体
  • 内部存储了函数的地址和其捕获变量的地址
  • 既然是地址,刚刚执行的时候能够累加也就很正常了
  • 所以闭包是引用类型这个概念也得到了很好的验证

3.3 捕获多个值

上面我们分析了闭包捕获值的基本原理,但是也仅仅是捕获了一个值,如果要捕获多个值呢?

3.3.1 捕获2个值

示例代码:

func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

转换为IR代码

image

根据图片中的解释,对于要捕获的两个值,这里是amountrunningTotal

  1. 首先是为这两个值开辟堆空间
  2. 然后是构建一个结构体用于存储runningTotal及相关信息
  3. 接下来又构建了一个结构体,存储一些信息和指向2的地址,还有amount
  4. 最后将函数地址和指向3中内容的指针构建成一个结构体进行返回

按照这个逻辑我们再次仿写一下:

// 函数对应的结构体
// 此处的泛型是第二个结构体成员指针对应的实际结构
struct FuntionData{
    // 函数地址
    var ptr: UnsafeRawPointer
    // 捕获值的指针
    var captureValue: UnsafePointer
}

// 捕获值的结构体
struct BoxType {
    var refCounted: HeapObject
    var boxValue:UnsafePointer
    //捕获的值,此处是Int
    var value: Int
}

struct Box {
    // HeapObject
    var refCounted: HeapObject
    //捕获的值
    var value: T
}

// HeapObject
struct HeapObject{
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
    var f: () ->Int
}

func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let makeInc = makeIncrementer(forIncrement: 11)
let f = VoidIntFun(f: makeInc)

// 初始化内存空间
let ptr = UnsafeMutablePointer.allocate(capacity: 1)
ptr.initialize(to: f)

// 内存重新绑定
let ctx = ptr.withMemoryRebound(to: FuntionData>>.self, capacity: 1){$0.pointee}

print(ctx.ptr)
print(ctx.captureValue)
print(ctx.captureValue.pointee.value)
print(ctx.captureValue.pointee.boxValue.pointee.value)


0x0000000100002800
0x000000010061dee0
11
10

使用nm命令查看,结果一致:


image

读一下captureValue对于的内存,结果如下:

image

3.3.1 捕获3个值

示例代码:

func makeIncrementer(forIncrement amount: Int, amount1: Int) -> () -> Int{
    var runningTotal = 10
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        runningTotal += amount1
        return runningTotal
    }
    return incrementer
}

IR代码:

image

我们可以看把%0和%1依次存储到%10的连续空间,也就是说我们的BoxType应该是这样的:

//// 捕获值的结构体
struct BoxType {
    var refCounted: HeapObject
    var boxValue:UnsafePointer
    //捕获的值,此处是Int
    var value1: Int
    var value2: Int
}

那么如果不是参数形式传递过来的呢?修改代码为如下:

func makeIncrementer(forIncrement amount: Int) -> () -> Int{
    var runningTotal = 10
    var tempaaaaaaa = 12
    //内嵌函数,也是一个闭包
    func incrementer() -> Int{
        runningTotal += amount
        tempaaaaaaa += amount
        return runningTotal + tempaaaaaaa
    }
    return incrementer
}
image

我们可以看到,对于直接定义的变量存储到了跟原来runningTotal一致的位置,所以对应的BoxType应该是这样的:

struct BoxType {
    var refCounted: HeapObject
    var boxValue:UnsafePointer
    //捕获的值,此处是Int
    var value: Int
}

struct Box {
    // HeapObject
    var refCounted: HeapObject
    //捕获的值
    var value1: T
    var value2: T
}

所以对于直接定义的变量和参数传入的变量,在结构上是分开存储的。

如果我们把上面的Int都换成String

func makeIncrementer(forIncrement name: String) -> () -> String{
    var address = "beijing"
    var street = "xizhimen"
    //内嵌函数,也是一个闭包
    func incrementer() -> String{
        address += name
        street += "32"
        return address + street
    }
    return incrementer
}

查看IR代码:


Xnip2021-03-09_16-25-16

此时我们在看IR代码就是上图的样子了

如果转换为BoxType就是如下的代码:

struct BoxType {
    var refCounted: HeapObject
    var boxValue:UnsafePointer
    //捕获的值,此处是Int
    var value: String
    var boxValue2:UnsafePointer
}

struct Box {
    // HeapObject
    var refCounted: HeapObject
    //捕获的值
    var value: T
}

3.4 小结

通过IR代码我们知道了闭包捕获值的原理,现在总结如下:

  1. 闭捕获值的本质是在堆区开辟内存然后存储其在上下文中捕获到的值
  2. 修改值也是修改的堆空间的值
  3. 闭包是一个引用类型
  4. 闭包的底层结构是一个结构体
    1. 首先存储闭包的地址
    2. 加上捕获值的地址
  5. 在捕获的值中,会对定义的变量和函数中的参数分开存储
  6. 存储的时候内部会有一个HeapObject结构,用于管理内存,引用计数
  7. 函数是特殊的闭包,只不过函数不捕获值,所以在闭包结构体中只存储函数地址,不存储指向捕获值的指针

4. 逃逸闭包&非逃逸闭包

在开发中我们比较关心的就是逃逸闭包和非逃逸闭包了,那么这两种闭包有什么区别呢?

4.1 非逃逸闭包

Swift 3.0之后,系统默认闭包参数就是被@noescapeing修饰的,这点我们可以通过sil代码看出来。

func test(by: ()->()) {
    by()
}
image

对于非逃逸闭包具有以下特性:

  1. 执行时机:在函数体内执行
  2. 声明周期:在函数执行完后,闭包也就消失了

4.2 逃逸闭包

逃逸闭包的定义:当闭包作为一个实际参数传递给一个函数的时候,并且是在函数返回之后调用,我们就说这个闭包逃逸了。当我们声明一个接受闭包作为形式参数的函数时,你可以在形式参数前些@escapeing来明确闭包是允许逃逸的。

另外使用@escapeing修饰的闭包在其闭包体内需要显示的使用self,对于显示的使用self主要是希望开发者注意循环引用。

逃逸闭包的使用时机分为以下两种情况:

  1. 保存在一个函数外部定义的变量中
  2. 延迟调用

4.2.1 外部定义的变量中

对于外部定义的变量就是我们常说的属性了,下面我们看看这个例子:

示例代码:

class Person {
    
    var completionHandler: ((Int)->Void)?
    
    func doSomething(complition: @escaping (Int) -> Void){
        self.completionHandler = complition
    }
}

var p = Person()
p.doSomething{
    print($0)
}

if let c = p.completionHandler {
    c(10)
}
  • 此处就是在类中定义一个闭包属性
  • 在类中的一个函数中给这个属性赋值
  • 当函数返回后,在合适的时机才会执行闭包
  • 很显然闭包的声明周期要比函数的生命周期长

4.2.2 延迟调用

关于延迟调用,主要用于网络请求的异步回调,我们还是来看一个例子:

建议创建一个iOS工程去执行,如果是Macos的容易直接进程结束,出不来结果。

import Foundation

class Person {

    func doSomething(complition: @escaping (Int) -> Void){
        DispatchQueue.global().asyncAfter(deadline: .now()+0.1) {
            print("延迟执行")
            complition(10)
        }
        
        print("函数执行完了")
    }
}

var p = Person()
p.doSomething {
    let a = $0 + 1
    print(a)
}


函数执行完了
延迟执行
11

同样还是因为方法的生命周期不如闭包的生命周期长,当方法返回后,延迟一段时间才会执行闭包。

4.3 小结

关于逃逸闭包和非逃逸闭包基本就说完了,下面稍作总结:

  1. 首先两者都是作为函数参数
  2. 非逃逸闭包在函数中调用,并且是函数结束前就调用完成
  3. 非逃逸闭包不会产生循环引用,因为作用域在函数内,函数执行完毕会释放函数内的所有对象
  4. 针对非逃逸闭包编译器会优化内存管理的调用
  5. 根据官方文档的说明,非逃逸闭包会保存在栈上(未验证出来)
  6. 逃逸闭包在函数返回后才会调用,也就是闭包逃离出了函数的作用域
  7. 逃逸闭包中可能会产生循环引用
  8. 逃逸闭包中需要显示的引用self,主要作用是提醒开发者注意循环引用

5. 自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的大括号,用一个普通的表达式来代替显示的闭包。

自动闭包能够让你延迟求值,因为直到你调用这个闭包,代码段才会被执行。

举个例子:

func debugOutPrint(_ condition: Bool, _ message: String){
//    #if DEBUG
//    print(message)
//    #endif
    
    if condition { print(message) }
}
debugOutPrint(true, "Application Error Occured")

我们在开发过程中都会打印日志,但是上线后就不需要打印这些日志了,所以一般会封装一个全局函数用来控制打印。也就是上面这段代码,这里为了很好的说明问题我们使用condition来控制是否打印。

如果我们需要打印的字符串是从某个业务逻辑中获取到的:

func debugOutPrint(_ condition: Bool, _ message: String){
    if condition { print(message) }
}

func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}

// 如果传入true
debugOutPrint(true, doSomething())

doSomething
Network Error Occured

// 如果传入false
debugOutPrint(false, doSomething())

doSomething

通过打印结果我们可以看到,无论传入的是false还是true都会触发dosomething的打印。如果这个方法中有很多耗时操作,那么对资源的浪费就是必然的,所以为了避免这种情况我们可以通过闭包去解决问题。

func debugOutPrint(_ condition: Bool, _ message: () -> String){
    if condition { print(message()) }
}

func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}

此时,如果在传入false就不会打印doSomething了,也就是不会执行doSomething方法。

虽然使用闭包会使函数延迟执行,但是现在我们如果想传入一个String就会报错了,那么解决办法就是使用自动闭包:

func debugOutPrint(_ condition: Bool, _ message: @autoclosure() -> String){
    if condition { print(message()) }
}

func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}

debugOutPrint(true, doSomething())
debugOutPrint(false, doSomething())

debugOutPrint(true, "Application Error Occured")

以上就是应用了自动闭包延迟执行和自动包装对象的特性。

5. 闭包的循环引用

关于闭包的循环引用,请参考我的另一篇文章Swift 循环引用

6. 函数

上面多次提到函数是引用类型,下面我们也来验证一下:

我们编写如下代码:

func makeIncrementer(inc: Int) -> Int{
    var runningTotal = 10
    runningTotal += inc
    return runningTotal
}

var makeInc = makeIncrementer

生成sil代码:

image

以上sil代码是将函数赋值给makeInc变量的过程,其中我们可以看到一个thin_to_thick_function关键字,看字面意思是瘦到胖函数,下面我们在sil文档中查询一下这个关键字的含义thin_to_thick_function

image

译:将一个瘦函数值,即一个没有上下文信息的空函数指针,转换成一个忽略上下文的胖函数值。应用得到的thick函数值等价于应用原始thin值。如果证明不需要上下文,thin_to_thick_function转换可能会被消除。

根据译文,我们知道这里是对函数进行处理,但是我们并没有看见makeInc变量的存储结构是什么样的,所以我们还需要在看看IR代码:

image

我们可以看到此处与闭包类似,同样是一个swift.refcounted结构体,开始存了个函数指针,后面存了null,也就是不需要捕获上下文中的变量,所以就不存这个捕获值的指针了。

下面我们使用Swift代码仿写一下:

func makeIncrementer(inc: Int) -> Int{
    var runningTotal = 10
    runningTotal += inc
    return runningTotal
}

var makeInc = makeIncrementer

struct FuntionData{
    // 函数地址
    var ptr: UnsafeRawPointer
    // 此处应该是个null
    var captureValue: UnsafeRawPointer?
}

//封装闭包的结构体,目的是为了使返回值不受影响
struct VoidIntFun {
    var f: (Int) ->Int
}

let ptr = UnsafeMutablePointer.allocate(capacity: 1)
ptr.initialize(to: VoidIntFun(f: makeInc))
let ctx = ptr.withMemoryRebound(to: FuntionData.self, capacity: 1){$0.pointee}

print(ctx.ptr)
print(ctx.captureValue)


0x0000000100003090
nil

使用cat adress命令查看该地址:

image

可以看到,这个地址对应的函数就是makeIncrementer

  • 函数也是引用类型
  • 函数在底层也是个结构体
  • 函数和闭包的存储是差不多的
  • 只不过函数不捕获上下文中的值
  • 所以相对于闭包没有指向捕获值的指针

你可能感兴趣的:(Swift 闭包)