Swift闭包

闭包, 一个捕获了全局上下文的常量或者变量的函数闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。

一、闭包的使用

全局函数 是一种特殊的闭包,定义一个全局函数,只是当前的全局函数并不捕获值。

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

函数闭包:下面的函数是一个闭包,函数中的incrementer是一个内嵌函数,可以从makeIncrementer中捕获变量runningTotal

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

let incre = makeIncrementer()
print(incre())

闭包表达式 / 匿名函数:下面是一个闭包表达式,即一个匿名函数,而且是从上下文中捕获变量和常量。

//闭包表达式
{ (param) -> ReturnType in
    //方法体
}

闭包表达式特点:是一个匿名函数,所有代码都在花括号{}内,参数和返回类型in关键字之前,in关键字之后是主体内容(类似方法体)。

尾随闭包

当闭包作为函数的最后一个参数,如果当前的闭包表达式很长,我们可以通过尾随闭包的书写方法来提高代码的可读性。

//闭包表达式作为函数的最后一个参数
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)
}

array.sorted就是一个尾随闭包,且这个函数就只有一个参数,如下所示:

//array.sorted就是一个尾随闭包
var array = [1, 2, 3]
//1、完整写法
array.sorted { (item1: Int, item2: Int) -> Bool in return item1 < item2}
//2、省略参数类型:通过array中的参数推断类型
array.sorted { (item1, item2) -> Bool in return item1 < item2}
//3、省略参数类型 + 返回值类型:通过return推断返回值类型
array.sorted { (item1, item2) in return item1 < item2}
//4、省略参数类型 + 返回值类型 + return关键字:单表达式可以隐士表达,即省略return关键字
array.sorted { (item1, item2) in item1 < item2}
//5、参数名称简写
array.sorted {return $0 < $1}
//6、参数名称简写 + 省略return关键字
array.sorted {$0 < $1}
//7、最简:直接传比较符号
array.sorted (by: <)

闭包的有点
利用上下文推断参数和返回类型
单表达式可以隐式返回,省略return关键字;
参数名称可以直接使用简写(如$0,$1,元组的$0.0);
尾随闭包可以更简洁的表达。

二、变量捕获

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

let incre = makeIncrementer()
print(incre())
print(incre())
print(incre())
//打印结果:11  12  13

结果为什么是累加的:因为内嵌函数捕获了runningTotal,不再是单纯的一个变量了。
换一种方式:

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

老司机应该能猜到结果,下面我们深入分析一下原因。

2.1 SIL

SIL命令:swiftc -emit-sil ${SRCROOT}/SwiftTest/main.swift | xcrun swift-demangle > ./main.sil && open main.sil

image.png

1、通过alloc_box申请了一个上的空间,并将引用计数地址给了RunningTotal,将变量存储到上;
2、通过project_box从堆上取出变量;
3、将取出的变量交给闭包进行调用。
结论:捕获值的本质是将变量存储到堆上

LLDB:

image.png
image.png
通过断点,看到在makeIncrementer方法内部调用了swift_allocObject方法。

总结:
1、一个闭包能够从上下文捕获已经定义的常量和变量,并且能够在其函数体内引用和修改这些值,即使这些定义的常量和变量的原作用域不存在;
2、修改捕获值实际是修改堆区中的value值
3、当每次重新执行当前函数时,都会重新创建内存空间。

三、逃逸闭包 & 非逃逸闭包

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

  • 如果用@escaping修饰闭包后,我们必须显示的在闭包中使用self
  • swift3.0之后,系统默认闭包参数就是被@nonescaping修饰。
    image.png

逃逸闭包的使用场景,①延迟调用,②作为属性存储,在后面进行调用。
延迟调用
1、在延迟方法中调用逃逸闭包

class Animal {
    //定义一个闭包属性
    var complitionHandler: ((Int)->Void)?
    
    //函数参数使用@escaping修饰,表示允许函数返回之后调用
    func makeIncrementer(amount: Int, handler: @escaping (Int)->Void){
        var runningTotal = 0
        runningTotal += amount
        //赋值给属性
        self.complitionHandler = handler
        
        //延迟调用
        DispatchQueue.global().asyncAfter(deadline: .now()+0.1) {
            print("逃逸闭包延迟执行")
            handler(runningTotal)
        }
        print("函数执行完了")
    }

    func doSomething(){
        self.makeIncrementer(amount: 10) {
            print($0)
        }
    }

    deinit {
        print("Animal deinit")
    }
}

var t = Animal()
t.doSomething()
sleep(2)
//打印结果:
//函数执行完了
//逃逸闭包延迟执行
//10

当前方法执行的过程中不会等待闭包执行完成后再执行,而是直接返回,所以当前闭包的生命周期要比方法长

作为属性

当闭包作为存储属性时,主要有以下几点说明:
1、定义一个闭包属性;
2、在方法中对闭包属性进行赋值;
3、在合适的时机调用(与业务逻辑相关)。

观察上一段代码,complitionHandler作为Animal的属性,是在方法makeIncrementer调用完成后才会调用,这时,闭包的生命周期要当前方法的生命周期长

逃逸闭包 vs 非逃逸闭包 区别
  1. 非逃逸闭包:一个接受闭包作为参数的函数,闭包是在这个函数结束前被调用,即可以理解为闭包是在函数作用域结束前被调用。
    1.1 不会产生循环引用,因为闭包的作用域在函数作用域内,在函数执行完成后,就会释放闭包捕获的所有对象;
    1.2 针对非逃逸闭包,编译器会做优化:省略内存管理调用
    1.3 非逃逸闭包捕获的上下文保存在栈上,而不是堆上(官方文档说明)。
  2. 逃逸闭包:一个接受闭包作为参数的函数,逃逸闭包可能会在函数返回之后才被调用,即闭包逃离了函数的作用域。
    2.1 可能会产生循环引用,因为逃逸闭包中需要显式的引用self(猜测其原因是为了提醒开发者,这里可能会出现循环引用了),而self可能是持有闭包变量的(与OC中block的的循环引用类似);
    2.2 一般用于异步函数的返回,例如网络请求。
  3. 使用建议:如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包。
  4. 总结:主要区别就是调用时机和内存管理不同

四、自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包花括号,用一个普通的表达式来代替显式的闭包
有下面一个例子,当conditiontrue时,会打印错误信息,即,如果是false,当前条件不会执行。

//1、condition为false时,当前条件不会执行
func debugOutPrint(_ condition: Bool, _ message: String){
    if condition {
        print("cjl_debug: \(message)")
    }
}
debugOutPrint(true, "Application Error Occured")

如果字符串是在某个业务逻辑中获取的

func debugOutPrint(_ condition: Bool, _ message: String){
    if condition {
        print("animal_debug: \(message)")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}

//如果传入true
debugOutPrint(true, doSomething())
//打印结果:animal_debug: Network Error Occured

//如果传入false
debugOutPrint(false, doSomething())
//打印结果:doSomething

此时,无论是传入true还是false,当前的方法doSomething都会执行,如果这个方法是一个非常耗时的操作,这里就会造成一定的资源浪费。所以为了避免这种情况,需要将当前参数修改为一个闭包。
修改:将message参数修改成一个闭包,需要传入的是一个函数。即,需要时在调用延缓了方法的调用时机。

//3、为了避免资源浪费,将当前参数修改成一个闭包
func debugOutPrint(_ condition: Bool, _ message: () -> String){
    if condition {
        print("animal_debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}
debugOutPrint(true, doSomething)

如果这里既可以传string,也可以传闭包,可以实现吗?
可以通过@autoclosure将当前的闭包声明成一个自动闭包不接收任何参数,返回值是当前内部表达式的值。所以当传入一个String时,其实就是将String放入一个闭包表达式中,在调用的时候返回。

//4、将当前参数修改成一个闭包,并使用@autoclosure声明成一个自动闭包
func debugOutPrint(_ condition: Bool, _ message: @autoclosure() -> String){
    if condition {
        print("cjl_debug: \(message())")
    }
}
func doSomething() -> String{
    print("doSomething")
    return "Network Error Occured"
}

//使用1:传入函数
debugOutPrint(true, doSomething())

//使用2:传入字符串
debugOutPrint(true, "Application Error Occured")

debugOutPrint(true, "Application Error Occured")这句代码等价于:

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

推荐阅读更多精彩内容

  • Swift 闭包 [TOC] 前言 我个人觉得在看这篇文章前,先了解一下Swift 函数[https://swif...
    just东东阅读 730评论 0 2
  • 前言 本篇文章主要讲解Swift中又一个相当重要的知识点 👉 闭包,首先会介绍闭包的概念,包含与OC中Block的...
    深圳_你要的昵称阅读 473评论 0 9
  • 闭包的定义闭包是一个捕获了上下文的常量或变量的匿名函数。 👆的全局函数是一种特殊的闭包,不捕获变量; 👇的内嵌函数...
    YY323阅读 298评论 0 6
  • 闭包 闭包是⼀个捕获了上下⽂的常量或者是变量的函数。 上⾯的函数是⼀个全局函数,也是⼀种特殊的闭包,只不过当前的全...
    Mjs阅读 227评论 0 0
  • 闭包 闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift中的闭包与C和Objective-C中的代码块...
    浪的出名阅读 708评论 0 1