swift进阶十三:闭包

swift进阶 学习大纲

swift进阶八:闭包 & Runtime & Any等类型 中,我们介绍了闭包 捕获变量特性。本节,我们继续了解闭包:

  1. 什么是闭包
  2. 闭包结构
  3. 逃逸闭包非逃逸闭包
  4. 自动闭包
  5. 函数作形参

1. 什么是闭包

来自维基百科的解释:

  • 在计算机科学中,闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数编程语言中实现词法绑定的一种技术

  • 闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联环境(相当于一个符号查找表

1.1 全局函数

一种特殊闭包

  • 无捕获变量全局函数,也属于闭包
func test(){
     print(a)
}

1.2 内嵌函数

也是闭包,会捕获外部变量
incrementer内嵌函数,也是一个闭包,捕获了上层函数runningTotal

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

print(makeIncrementer())   // (()-> Int)匿名函数
print(makeIncrementer()()) // Int

打印值:

image.png

1.3 闭包表达式

  • 闭包是一个匿名函数
  • 所有代码都在花括号内
  • 参数返回类型in关键字之前
  • in之后是主体内容
{ (参数)-> 返回类型 in
    // do something
}
  • swift的闭包可以当作let 常量var 变量,也可以当作参数传递。
// 常量
let closure1: (Int) -> Int

// 变量
var closure2 : (Int) -> Int = { (age: Int) in 
     return age
}

// 参数传递
func test(params: ()->Int) {
    print(params())
}

var age = 10
// 执行(尾随闭包)
test { () -> Int in
    age += 1
    return age
}
  • swift闭包支持可选类型:在()外使用声明
// 可选值:在()外使用?声明
var closure: ((Int) -> Int)?

1.4 尾随闭包

闭包表达式作为函数最后一个参数时,可通过尾随闭包书写方式提高代码的可读性

func test(_ a: Int, _ b: Int, _ c: Int, by: (_ item1: Int, _ item: Int, _ item3: Int) -> Bool) -> Bool{
    return by(a, b, c)
}

// 常规写法
test(10, 20, 30, by: { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
    return itme1 + itme2 < itme3
})

// 快捷写法(小括号提到最后一个参数前)
test(10, 20, 30) { (_ itme1: Int, _ itme2: Int, _ itme3: Int) -> Bool in
    return itme1 + itme2 < itme3
}

// 最简洁写法 (入参直接使用$0 $1 $2代替,单行代码可省略return)
test(10, 20, 30) { $0 + $1 < $2 }

可以看到,最简洁写法看上去非常舒服语义表达清晰

闭包表达式swift语法。使用闭包表达式可以更简洁传递信息。好处多多:

  • 利用上下文推断参数返回类型
  • 单表达式可以隐式返回省略return关键字
  • 参数名称可以直接使用简写(如$0,$1,元组的$0.0)
  • 尾随闭包可以更简洁的表达
var array = [1, 2, 3]

// 1. 常规写法
array.sort(by: {(item1 : Int, item2: Int) -> Bool in return item1 < item2 })

// 2. 省略类型 (根据上下文,可自动推断参数类型)
array.sort(by: {(item1, item2) -> Bool in return item1 < item2 })

// 3. 省略返回类型 (根据上下文,可自动推断返回类型)
array.sort(by: {(item1, item2) in return item1 < item2 })

// 4. 省略return (单行表达式,可省略return)
array.sort{(item1, item2) in item1 < item2 }

// 5. 参数简写 (使用$0 $1,按位置顺序获取参数)
array.sort{ return $0 < $1 }

// 6. 省略return (单行表达式,可省略return)
array.sort{ $0 < $1 }

// 7. 使用高阶函数,传递排序规则
array.sort(by: <)

2. 闭包的结构

  • 我们使用变量记录闭包,发现内部属性也被记录了。
    (下面runningTotal被记录,多次打印时,runningTotal结果不一样。
    如果直接调用闭包,会发现runningTotal结果一样)
func makeIncrementer() -> (()-> Int){
    var runningTotal = 10
    
    // 内嵌函数(也是一个闭包,捕获了runningTotal)
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    
    return incrementer
}

// 函数变量 (存储格式是怎样?)
var makeInc = makeIncrementer()

print(makeInc()) // 打印: 11
print(makeInc()) // 打印: 12
print(makeInc()) // 打印: 13

print(makeIncrementer()()) // 打印: 11
print(makeIncrementer()()) // 打印: 11
print(makeIncrementer()()) // 打印: 11

Q: 把函数赋值给变量,变量存储的是函数地址还是什么

  • SIL中看不到makeInc的结构:
image.png
  • 我们再往后一层,直接查看ir代码:

拓展IRIR语法:

image.png

Swift编译流程

  1. swift源码编译为AST语法树
    swiftc -dump-ast HTPerson.swift > ast.swift
  2. 生成SIL源码
    swiftc -emit-sil HTPerson.swift > ./HTPerson.sil
  3. 生成IR中间代码
    swiftc -emit-ir HTPerson.swift > ir.swift
  4. 输出.o机器文件
    swiftc -emit-object HTPerson.swift

ir语法 👉 官方文档

  • 数组
[<elementnumber> x <elementtype>]  // [数组梳理 x 数组类型]
// example
alloca[24 x i8],align8 // 24个i8都是0
  • 结构体
%swift.refcounted = type { %swift.type*, i64 }   // { 指针类型,类型}
// example
%T = type {<type list>}  // 和C语言结构体类似 
  • 指针类型
<type> *
// example
i64*    // 64位的整形
  • getelementptr指针别名
    可通过getelementptr读取数组结构体的成员:
<result> = getelementptr <ty>, <ty>* <ptrval> {, [inrange] <ty> <id x>}*
<result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
  • getelementptr读取案例
    ( 创建一个c语言工程,main.c中加入测试代码
// example
struct munger_struct {
        int f1;
        int f2;
};

void munge(struct munger_struct *p) { 
   p[0].f1 = p[1].f1 + p[2].f2;  // 假设P是有3个元素的数组。就可以直接通过下标读取
}
 
struct munger_struct array[3];

int main(int argc, const char * argv[]) {
   munge(array); //调用
   return 0;
}
  • LLVM中,C语言需要使用Clang输出IR文件:
    clang -S -fobjc-arc -emit-llvm main.c
    image.png
  • 熟悉了IR语法后,回到这个代码:
func makeIncrementer() -> (()-> Int){
    var runningTotal = 10
    func incrementer() -> Int {
        runningTotal += 1
        return runningTotal
    }
    return incrementer
}
// 函数变量 (存储格式是怎样?)
var makeInc = makeIncrementer()
  • 输出IR文件,分析结构:

    image.png

  • 按照IR分析的代码,我们自定义结构,读取函数指针地址内部属性值

由于无法直接读取()-> Int函数的地址,所以利用结构体地址就是首元素地址的特性,将函数设为结构体第一个属性。通过读取结构体指针地址,获取到函数地址

struct FunctionData<T> {
    var pointer: UnsafeRawPointer
    var captureValue: UnsafePointer<T>
}

struct Refcounted {
    var pointer: UnsafeRawPointer
    var refCount: Int64
}

struct Type {
    var type: Int64
}

struct Box<T> {
    var refcounted: Refcounted
    var value: T  // 8字节类型,可由外部动态传入
}

struct BoxMetadata<T> {
    var refcounted: UnsafePointer<Refcounted>
    var undefA: UnsafeRawPointer
    var type: Type
    var undefB: Int32
    var undefC: UnsafeRawPointer
}

// 由于无法直接读取`()-> Int`函数的地址,所以利用结构体地址就是首元素地址的特性。将函数设为结构体第一个属性
struct VoidIntFunc {
    var f: ()->Int
}

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

// 使用struct包装的函数
var makeInc = VoidIntFunc(f: makeIncrementer())

// 读取指针
let ptr = UnsafeMutablePointer<VoidIntFunc>.allocate(capacity: 1)

// 初始化
ptr.initialize(to: makeInc)

// 将指针绑定为FunctionData<Box<Int>>类型,返回指针
let context = ptr.withMemoryRebound(to: FunctionData<Box<Int>>.self, capacity: 1) { $0.pointee }

// 打印指针值 与 内部属性值
print(context.pointer)   // 打印 0x00000001000054b0
print(context.captureValue.pointee.value) // 打印  10
image.png

验证打印的函数地址是否是makeIncrementer函数:

image.png

打开终端,输入命令:nm -p 编译后的machO文件地址 | grep 函数地址

// 例如:
nm -p /Users/asetku/Library/Developer/Xcode/DerivedData/Demo-bhpsxmnrzusvmeaotyclgmelcxpp/Build/Products/Debug/Demo | grep 00000001000054b0
  • 可以看到,该地址正是函数地址
    image.png
  • 使用xcrun swift-demangle XXXX命令,还原函数名
    image.png

所以:
函数事故引用类型,被赋值的变量记录了函数地址
函数内变量,会alloc开辟空间,调用前后,会retainrelease管理引用计数

拓展: 如果函数内部多个属性结构是怎样呢?

func makeIncrementer() -> (()-> Int){
   var aa = 10
   var bb = 20
   // 内嵌函数(也是一个闭包,捕获了runningTotal)
   func incrementer() -> Int {
       aa += 6
       bb += 9
       return bb
   }
   return incrementer
}
var makeInc = makeIncrementer()
  1. 基础结构没有变化


    image.png
  2. 相比起单变量,多了一个临时结构,把两个变量分别用指针记录:

    image.png

struct FunctionData<T> {
   var pointer: UnsafeRawPointer
   var captureValue: UnsafePointer<T>
}

struct Refcounted {
   var pointer: UnsafeRawPointer
   var refCount: Int64
}

struct Type {
   var type: Int64
}

struct Box<T> {
   var refcounted: Refcounted
   var value: T  // 8字节类型,可由外部动态传入
}

// 多了一个Box2结构,每个变量都是`Box`结构的对象
struct Box2<T> {
   var refcounted: Refcounted
   var value1: UnsafePointer<Box<T>>
   var value2: UnsafePointer<Box<T>>
}

struct BoxMetadata<T> {
   var refcounted: UnsafePointer<Refcounted>
   var undefA: UnsafeRawPointer
   var type: Type
   var undefB: Int32
   var undefC: UnsafeRawPointer
}

// 由于无法直接读取`()-> Int`函数的地址,所以利用结构体地址就是首元素地址的特性。将函数设为结构体第一个属性
struct VoidIntFunc {
   var f: ()->Int
}

func makeIncrementer() -> (()-> Int){
  var aa = 10
  var bb = 20
  // 内嵌函数(也是一个闭包,捕获了runningTotal)
  func incrementer() -> Int {
      aa += 6
      bb += 9
      return bb
  }
  return incrementer
}

// 使用struct包装的函数
var makeInc = VoidIntFunc(f: makeIncrementer())

// 读取指针
let ptr = UnsafeMutablePointer<VoidIntFunc>.allocate(capacity: 1)

// 初始化
ptr.initialize(to: makeInc)

// 将指针绑定为FunctionData<Box<Int>>类型,返回指针
let context = ptr.withMemoryRebound(to: FunctionData<Box2<Int>>.self, capacity: 1) { $0.pointee }

// 打印指针值 与 两个内部属性值
print(context.pointer)   // 打印: 0x00000001000050c0
print(context.captureValue.pointee.value1.pointee.value) // 打印:10
print(context.captureValue.pointee.value2.pointee.value) // 打印: 20
image.png
  • MachO文件中校验函数地址,确定是makeIncrementer函数:
    image.png

3. 逃逸闭包与非逃逸闭包

  • 逃逸闭包
    闭包作为一个实际参数传递给一个函数,且在函数返回之后调用,我们就说这个闭包逃逸了。
    (逃逸闭包作为函数形参时,需要使用@escaping声明,生命周期函数。如:被外部变量持有异步延时调用)
  • 非逃逸闭包
    系统默认闭包参数是@nonescaping声明, 是非逃逸闭包生命周期被调用函数保持一致

3.1 逃逸闭包

  • 闭包函数外变量持有,需要@escaping声明为逃逸闭包
  • 闭包异步线程延时调用,需要@escaping声明为逃逸闭包
class HTPerson {
    
    var completion: ((Int)->Void)?
    
    func test1(handler: @escaping (Int)->Void) {
        
        // 1. 外部变量持有handler,handler的生命周期逃到了`makeIncrementer`函数外
        self.completion = handler
    }
    
    func test2(handler: @escaping (Int)->Void) {
        
        // 2. 异步线程延时调用handler,handler的生命周期逃到了`makeIncrementer`函数外
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            handler(10)
        }
    }
    
}

3.2 非逃逸闭包

系统默认闭包为非逃逸闭包 ,编译期自动加上@noescape声明,生命周期函数一致

func test(closure: (()->())) {}
  • SIL编译可以看到默认使用@noescape声明闭包:
    image.png

4. 自动闭包

  • 自动闭包自动识别闭包返回值,可直接接收返回值类型数据
    需要用@autoclosure声明,不接收任何参数返回值是当前内部表达式(如()->String)
// 自动闭包`@autoclosure`声明
func testPrint(_ message: @autoclosure ()->String) {
    print(message())
}

func doSomeThing() -> String {
    return "吃了吗?"
}
// 入参传`函数`
testPrint(doSomeThing()) 
// 入参传`字符串`
testPrint("干啥呢")
  • 可以看到,使用自动闭包时,参数可以是函数,也可以是闭包返回类型(字符串).
    自动闭包可以兼容函数入参类型(函数/函数返参类型

5. 函数作形参

  • 函数作为参数进行传递时,可节省计算量,在合适时期执行

  • 普通函数:
    doSomeThing是一个耗时操作计算结果传给testPrint时,testPrint由于条件不满足,压根没用到这个耗时操作结果

func testPrint(_ condition: Bool, _ message: String) {
    if condition {
        print("错误信息: \(message)")
    }
    print("结束")
}

func doSomeThing() -> String {
    print("执行了")
    // 耗时操作,从0到1000拼接成字符串
    return (0...1000).reduce("") { $0 + " \($1)"}
}

testPrint(false, doSomeThing())
image.png
  • 使用函数作为入参:
    入参直接传入函数未满足条件时,执行函数,避开了耗时操作
func testPrint(_ condition: Bool, _ message: ()->String) {
    if condition {
        print("错误信息: \(message())")
    }
    print("结束")
}

func doSomeThing() -> String {
    print("执行了")
    // 耗时操作,从0到1000拼接成字符串
    return (0...1000).reduce("") { $0 + " \($1)"}
}

testPrint(false, doSomeThing())
image.png

注意

  • 上述只是演示函数作为入参没有单次调用时,可延时合适时期调用,避免资源提前计算
  • 但如果该资源会被多次调用,还是提前计算资源节省资源

至此,我们对闭包有了完整认识。下一节介绍Optional & Equatable & 访问控制

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容