闭包捕获语义第一弹:一网打尽!

作者:Olivier Halligon,原文链接,原文日期:2016-07-25
译者:walkingway;校对:小锅;定稿:CMB

尽管现在已经是 ARC 的天下了,但对于程序员来说理解内存管理和对象的生命周期依然是一门必修课。对于在 Swift 当中广泛应用的闭包就是其中一个特殊的例子,与 Objc 的闭包相比,Swift 的闭包也有着不同的捕获语义。下面让我们看看闭包是如何工作的。

介绍

在 Swift 中,闭包捕获他们所引用的变量:虽然这些变量在闭包之外声明,但只要在闭包内使用都会默认被闭包保留引用(retain),这是为了确保闭包执行时,这些变量还活着(译者注:没有被提前释放)。

在文章接下来的部分,我们来定义一个简单的 Pokemon(口袋妖怪)类:

class Pokemon: CustomDebugStringConvertible {
  let name: String
  init(name: String) {
    self.name = name
  }
  var debugDescription: String { return "<Pokemon \(name)>" }
  deinit { print("\(self) escaped!") }
}

接下来声明一个简单的函数,他接受一个闭包作为参数,然后在一段时间后执行这个闭包(使用 GCD)。下面的例子展示了闭包是如何捕获外部变量的。

func delay(seconds: NSTimeInterval,closure: ()->()) {
  let time = dispatch_time(DISPATCH_TIME_NOW,Int64(seconds * Double(NSEC_PER_SEC)))
  dispatch_after(time,dispatch_get_main_queue()) {
    print("?")
    closure()
  }
}

在 Swift 3 中,上面的函数应该变成下面这种形式:

func delay(seconds: Int,closure: ()->()) {
  let time = DispatchTime.now() + .seconds(seconds)
  DispatchQueue.main.after(when: time) {
    print("?")
    closure()
  }
}

默认的捕获语义

现在,先从一个简单的例子开始:

func demo1() {
  let pokemon = Pokemon(name: "Mewtwo")
  print("before closure: \(pokemon)")
  delay(1) {
    print("inside closure: \(pokemon)")
  }
  print("bye")
}

这个例子看上去很简单,但它有趣的地方在于闭包的运行被推迟了 1 秒钟,所以当 demo1() 函数执行完毕后,闭包才开始执行;并且 1 秒后当闭包被执行的时候 Pokemon 实例依然存活着。

before closure: <Pokemon Mewtwo>
bye
?
inside closure: <Pokemon Mewtwo>
<Pokemon Mewtwo> escaped!

这是因为闭包捕获(强引用)了 pokemon 变量:编译器发现在闭包内部引用了 pokemon 变量,它会自动捕获该变量(默认是强引用),所以 pokemon 的生命周期与闭包自身是一致的。

因此,闭包有点像精灵球 ?,只要你持有着精灵球闭包,pokemon 变量也就会在那里,不过一旦精灵球闭包被释放,引用的 pokemon 也会被释放。

在这个例子中,一旦 GCD 执行完毕,闭包就会被释放,所以 pokemondeinit 方法也会被调用。

如果 Swift 没有自动捕获 pokemon 变量,这就意味着当执行到 demo1 函数结尾时,pokemon 变量将会脱离作用域,随后当闭包执行时,pokemon 就已经不存在了...这可能会导致程序崩溃。
幸亏 Swift 足够聪明,闭包会自动为我们捕获 pokemon。接下来我们会学习在必要时如何弱捕获(弱引用)这些变量。

被捕获的变量在执行时才取值

有一点值得注意的是 Swift 在闭包执行时才会取出捕获变量的值1。我们可以认为它之前捕获的是变量的引用(或指针)。

这里有一个有趣的例子:

func demo2() {
  var pokemon = Pokemon(name: "Pikachu")
  print("before closure: \(pokemon)")
  delay(1) {
    print("inside closure: \(pokemon)")
  }
  pokemon = Pokemon(name: "Mewtwo")
  print("after closure: \(pokemon)")
}

你能猜猜打印的结果吗?答案如下:

before closure: <Pokemon Pikachu>
<Pokemon Pikachu> escaped!
after closure: <Pokemon Mewtwo>
?
inside closure: <Pokemon Mewtwo>
<Pokemon Mewtwo> escaped!

请注意我们在创建完闭包之后修改了 pokemon 对象,闭包延迟一秒后执行(虽然此时已经脱离了 demo2() 函数的作用域),我们打印的结果是新的 pokemon 对象,而不是旧的!这是因为 Swift 默认捕获的是变量的引用。

具体的细节为:首先初始化一个值为 Pikachu 的 pokemon 对象,接着修改该对象的值为 Mewtwo(译者注:创建了新对象),之前值为 Pikachu 的对象由于没有其他变量强引用,所以会被释放。接着闭包等待一秒钟执行,打印捕获 pokemon 变量(引用)的内容。

这个特性对于值类型也是一样的,关于这一点或许会有些奇怪,比如下面例子中的 Int 类型:

func demo3() {
  var value = 42
  print("before closure: \(value)")
  delay(1) {
    print("inside closure: \(value)")
  }
  value = 1337
  print("after closure: \(value)")
}

打印结果:

before closure: 42
after closure: 1337
?
inside closure: 1337

你没看错,闭包打印了新的整型变量值---尽管整型变量是值类型!---因为它捕获了变量的引用,而不是变量自身的内容!

你可以修改闭包中捕获的变量

如果捕获的是变量 var(而不是常量 let),你也可以在闭包中2修改它的值。

func demo4() {
  var value = 42
  print("before closure: \(value)")
  delay(1) {
    print("inside closure 1,before change: \(value)")
    value = 1337
    print("inside closure 1,after change: \(value)")
  }
  delay(2) {
    print("inside closure 2: \(value)")
  }
}

代码的打印结果如下:

before closure: 42
?
inside closure 1,before change: 42
inside closure 1,after change: 1337
?
inside closure 2: 1337

变量 value 的值在闭包内部被修改了(尽管它已经被捕获了,但并不等同于一个常量拷贝,它依然保持着对原变量的引用)。接着第二个闭包执行时打印的就是这个新值了,此刻第一个闭包已经执行完毕并释放了所有的引用,而且 value 变量也脱离了 demo4() 函数的作用域。

捕获一个变量作为一个常量拷贝

如果想要在闭包创建时捕获变量的值,而不是在闭包执行时才去获取变量的值,你可以使用 捕获列表

捕获列表写在闭包的方括号之间,紧跟闭包的左括号(并且在闭包的参数或返回类型之前)3

在创建闭包时捕获变量的值(而不是变量的引用),你可以使用 [localVar = varToCapture] 捕获列表。看上去像这样:

func demo5() {
  var value = 42
  print("before closure: \(value)")
  delay(1) { [constValue = value] in
    print("inside closure: \(constValue)")
  }
  value = 1337
  print("after closure: \(value)")
}

打印结果:

before closure: 42
after closure: 1337
?
inside closure: 42

与上面的 demo3() 比较,这次闭包打印的是变量创建时的值,而不是后来赋的新值 1337,即使整个闭包的执行是在对变量重新赋值之后。

这就是 [constValue = value] 在闭包中所做的事情:在闭包创建时捕获变量 value 的内容 --- 而不是变量的引用。

回到 Pokemon 上

正如我们上面所看到的:如果一个变量是引用类型---就像我们的 Pokemon 类,闭包并没有真正(强引用)捕获变量的引用,而是捕获了一个针对原始实例 pokemon 的拷贝:

func demo6() {
  var pokemon = Pokemon(name: "Pikachu")
  print("before closure: \(pokemon)")
  delay(1) { [pokemonCopy = pokemon] in
    print("inside closure: \(pokemonCopy)")
  }
  pokemon = Pokemon(name: "Mewtwo")
  print("after closure: \(pokemon)")
}

这类似创建了一个中间变量指向同一个 pokemon,然后捕获了这个中间变量:

func demo6_equivalent() {
  var pokemon = Pokemon(name: "Pikachu")
  print("before closure: \(pokemon)")
  // here we create an intermediate variable to hold the instance 
  // pointed by the variable at that point in the code:
  let pokemonCopy = pokemon
  delay(1) {
    print("inside closure: \(pokemonCopy)")
  }
  pokemon = Pokemon(name: "Mewtwo")
  print("after closure: \(pokemon)")
}

事实上,使用捕获列表完全等同于上述代码的行为...除了中间变量 pokemonCopy 属于闭包的局部变量,只能在闭包内部访问。

相比 demo2() 直接使用 pokemondemo6() 则使用了 [pokemonCopy = pokemon] in …demo6() 输出如下:

before closure: <Pokemon Pikachu>
after closure: <Pokemon Mewtwo>
<Pokemon Mewtwo> escaped!
?
inside closure: <Pokemon Pikachu>
<Pokemon Pikachu> escaped!

以下是详细过程:

  • 皮卡丘(Pikachu)被创造。

  • 接着闭包捕获了 Pikachu 的拷贝(这里实际上是捕获了 pokemon 变量的值)。

  • 所以当我们紧接着为 pokemon 变量赋新值 “Mewtwo” 后,“Pikachu” 还没有被释放,依然被闭包所保留。

  • 当我们离开 demo6 函数的作用域,Mewtwo 就被释放了,在方法内部 pokemon 变量自身只被一个强引用所保持,离开作用域强引用也就消失了。

  • 稍后闭包执行时,打印了 "Pikachu",这是因为在闭包创建时,捕获列表就捕获了 Pokemon

  • 最后 GCD 释放了闭包,由此可以证明闭包保持了口袋妖怪皮卡丘(Pikachu pokemon)的引用。

与此刚好相反,我们来分析下 demo2 的代码:

  • 皮卡丘(Pikachu)被创造。

  • 闭包只捕获了 pokemon 变量的引用,而不是捕获其所包含的值 Pickachu

  • 所以当 pokemon 随后被分配了一个新值 "Mewtwo",此时没有任何对象的强引用指向 Pikachu,它也会立即被释放。

  • 因此闭包稍后执行打印的结果也是 Mewtwo

  • 在闭包执行完毕后 GCD 会释放闭包,此时 Mewtwo pokemon 会随闭包一起被释放。

知识点整合

上面的知识点都掌握了吗?我承认,确实有点多...

下面是一个人为设计的例子,它包含了闭包创建时就对变量取值---归功于捕获列表,以及先捕获变量的引用,而真正的取值放到闭包执行时这两种情形:

func demo7() {
  var pokemon = Pokemon(name: "Mew")
  print("➡️ Initial pokemon is \(pokemon)")

  delay(1) { [capturedPokemon = pokemon] in
    print("closure 1 — pokemon captured at creation time: \(capturedPokemon)")
    print("closure 1 — variable evaluated at execution time: \(pokemon)")
    pokemon = Pokemon(name: "Pikachu")
    print("closure 1 - pokemon has been now set to \(pokemon)")
  }

  pokemon = Pokemon(name: "Mewtwo")
  print("? pokemon changed to \(pokemon)")

  delay(2) { [capturedPokemon = pokemon] in
    print("closure 2 — pokemon captured at creation time: \(capturedPokemon)")
    print("closure 2 — variable evaluated at execution time: \(pokemon)")
    pokemon = Pokemon(name: "Charizard")
    print("closure 2 - value has been now set to \(pokemon)")
  }
}

能猜猜打印结果是什么吗?可能有点难猜,不过这是一个很好的练习,通过自己判断打印结果来测试你是否掌握了今天的课程...

下面是打印结果,你猜对了吗?

➡️ Initial pokemon is <Pokemon Mew>
? pokemon changed to <Pokemon Mewtwo>
?
closure 1 — pokemon captured at creation time: <Pokemon Mew>
closure 1 — variable evaluated at execution time: <Pokemon Mewtwo>
closure 1 - pokemon has been now set to <Pokemon Pikachu>
<Pokemon Mew> escaped!
?
closure 2 — pokemon captured at creation time: <Pokemon Mewtwo>
closure 2 — variable evaluated at execution time: <Pokemon Pikachu>
<Pokemon Pikachu> escaped!
closure 2 - value has been now set to <Pokemon Charizard>
<Pokemon Mewtwo> escaped!
<Pokemon Charizard> escaped!

所以,到底发生了什么?稍微有点复杂,让我给大家来逐步解释一下:

  1. 把 ➡️ pokemon 一开始设置为 Mew

  2. 创建闭包 1 并且它的新本地变量 capturedPokemon 捕获了 pokemon 的值(此刻 pokemon 的值为 New,并且闭包也捕获了 pokemon 变量的引用,capturedPokemonpokemeon 都会在闭包代码中使用)

  3. ? 然后将 pokemon 修改为 Mewtwo

  4. 创建闭包 2,它的新本地变量 capturedPokemon 捕获了 pokemon 的值(此刻 pokemon 的值为 Mewtwo,并且闭包也捕获了 pokemon 变量的引用,capturedPokemonpokemeon 都会在闭包代码中使用)

  5. 此刻,demo7() 函数已经执行完毕了

  6. 一秒钟后,GCD 执行第一个闭包

    • 它的打印结果为 Mew,即第二步创建闭包时捕获在 capturedPokemon 变量中的值

    • 它也会根据所捕获 pokemon 的引用,找出变量的当前值,它目前为 Mewtwo(至少是在第五步离开 demo7() 函数前的值)

    • 然后将变量 pokemon 的值改为 Pikachu(再次强调,闭包捕获的是变量 pokemon 的引用,所以 demo7() 函数中的 pokemon 变量与闭包中进行赋值操作的 pokemon 变量具有同的引用)

    • 当闭包执行完毕被 GCD 释放后,没有对象在强引用 Mew 了,因此会释放掉。但是第二个闭包的 capturedPokemon 依然捕获着 Mewtwo,并且第二个闭包也捕获了 pokemon 变量的引用,此刻它的值为 Pikachu

  7. ? 又过了一秒钟,GCD 开始执行第二个闭包

    • 它的打印结果为 Mewtwo,即步骤四第二个闭包创建时捕获在 capturedPokemon 变量中的值

    • 它也会根据所捕获 pokemon 的引用,找出变量的当前值,它目前为 Pikachu(因为在第一个闭包中已经修改了它)

    • 最后,将 pokemon 变量设置为 Charizard,由于 Pikachu 小精灵只被 pokemon 变量强引用,而此时 pokemon 已不再指向它了,所以也会立即被释放。

    • 当闭包执行完毕被 GCD 释放后,本地变量 capturedPokemon 脱离了作用域,所以 Mewtwo 会被释放,同时指向 pokemon 变量的强引用也会消失,小精灵 Charizard 也会被释放

总结

是不是感觉有点烧脑?这很正常,闭包捕获语义有时候会比较复杂,尤其类似最后那个例子。我们要记住下面几个关键点:

  • 在 Swift 闭包中使用的所有外部变量,闭包会自动捕获这些变量的引用

  • 在闭包执行时会根据这些变量引用得到所对应的具体值

  • 因为我们捕获的是变量的引用(而不是变量自身的值),所以你可以在闭包内部修改变量的值(当然变量要声明为 var,而不能是 let

  • 你可以在闭包创建时获取变量中的值,然后把它存储到本地常量中,而不是捕获变量的引用。我们可以使用带中括号的捕获列表来实现。

今天的课程就先学到这里,或许有些难以理解。不要犹豫,打开你的 Playground 尝试测试、修改、运行这些代码,直到你彻底理解了其中的原理。

一旦你理解了以上内容,就可以期待我的下一篇文章了,接下来我会讨论捕获弱变量(weakly)来避免循环引用,以及闭包中的 [weak self][unowned self] 意味着什么。

感谢 @merowing 和我在 Slack 上针对所有的闭包语义所做的讨论,包括在闭包执行时才对捕获变量取值的事实。大家感兴趣的话,可以访问他的 blog ?

  1. 对于熟悉 Objective-C 的同学已经注意到 Swift 的行为和 Objective-C 的默认闭包语义不同,而是有些类似于 Objective-C 中的变量带一个 __block 修饰符。

  2. 与 ObjC 的默认行为不同...更像是在 Objective-C 中使用 __block

  3. 注意即使在我们的例子中仅捕获了一个变量,在捕获列表中你可以列出不止一个捕获的变量,这就是为什么称它为列表(lists)的原因。并且即使没有显式地写出闭包参数列表,你依然要将 in 关键字放置于捕获列表的后面,和闭包正文分隔开来。

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

  1. 1
  2. 2
  3. 3

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。