swift中closure捕获列表的研究

本文相关知识参考了Apple官方文档泊学网

其实closure并不是什么新东西,如果你是从Objective-C转为swift开发的,那你可以很容易的理解它,它就相当于oc里的block,在swift里,我们称之为闭包(closure)。所谓循环引用,其实和oc一样,也就是对象之间的互相持有,造成互相都释放不了的情况。在siwft中我们可以通过捕捉列表(capture list)来解决它。我们一起通过几个例子来了解它们吧。

closure捕获的含义

如果我们粗略看过一遍Apple官方文档,或者以前做过oc开发,就很清楚闭包能够捕获其中的变量,但是呢,在swift中如果使用捕获列表,其实捕获的含义就变了,我们通过例子来具体看一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//值类型捕捉
var a = 1
var closureOne = {print(a)}
a = 2
closureOne()//打印2

//引用类型捕捉
class Person {
var name = "--"
}
var person = Person()
var closureTwo = {print(person.name)}

closureTwo()//打印--
person = Person()
person.name = "Peter"
closureTwo()//打印Peter

我们会发现,无论是值类型还是引用类型的捕捉,当aperson这两个变量在被closure第一次捕捉后,然后我们改变这两个变量的值,给他们赋予新的值,我们再次执行之前的closure,会发现都是打印的新值,而不是第一次的旧值,所以我们得出一个重要结论:

1.无论是值类型变量a,还是引用类型变量person,闭包都是捕获他们的引用,而不是他们引用的对象

2.closure表达式的值在定义时不会被评估,直到调用的时候才会被评估

变量a在这边虽然是值类型,但是这边确实是引用语义。

是不是不太好理解,我们这样来理解比较好,在这边,我们可以将a和person这两个变量想象为容器,容器里面装着1,2,或者不同的person实例。在这边我们只是捕捉了对容器的引用,至于容器里装了什么,我们不关心,它会随着外面的赋值而改变。

如果我们现在使用了capture list呢,我们来看一下代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//值类型捕捉
var a = 1
var closureOne = {[a] in print(a)}//增加了捕捉列表
closureOne()//打印1
a = 2
closureOne()//打印1

//引用类型捕捉
class Person {
var name = "--"
}
var person = Person()
var closureTwo = {[person] in print(person.name)}//增加了捕捉列表

person = Person()
person.name = "Peter"
closureTwo()//打印--

看上述代码,一旦我们使用了捕捉列表,我们会发现,无论变量a还是变量person,都是打印第一次的值,后续对这两个变量的更改,对closure的捕捉已经没有影响了。至此,我们可以得出另外一个重要结论:

1.在闭包内部,一旦使用capture list,那么对变量的捕捉不再是引用语义的捕获,而是值语义的捕获,捕获的是他们引用的对象

2.使用捕获列表后,在闭包的定义阶段,就已经捕获了变量

使用捕获列表解决循环引用的问题

在我们理解了捕获列表的真正含义之后,接下来我们就可以看一下捕获列表是如何解决循环引用问题的了。我们还是以例子来解释,如下:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
var name = "Peter"
var function: () -> () = {}
deinit {
print("Person deinit")
}
}

if true {
var person = Person()
}
//控制台最终打印“Person deinit”

上面的代码绝对没有问题,控制台也打印了”Person deinit”,说明person本成功释放,如果我们加两行行代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person {
var name = "Peter"
var function: () -> () = {}
deinit {
print("Person deinit")
}
}

if true {
var person = Person()
person.function = {print("My name is \(person.name)")}
person.function()
}
//控制台打印“My name is Peter”
//但是控制台没有打印“Person deinit”,person无法释放

很明显,上面的代码循环引用了,我们如何来处理它,首先,我们添加捕获列表,让闭包不再捕捉person的引用,而是捕获person引用的对象,如下:

1
2
3
4
5
6
7
if true {
var person = Person()
person.function = {[person] in print("My name is \(person.name)")}
person.function()
}
//控制台打印“My name is Peter”
//但是控制台没有打印“Person deinit”,person无法释放

但是这样的话,在闭包内,我们捕获到了person具体引用的对象,可是它引用的对象还是个引用类型,所以还是有循环引用,不过现在问题就熟悉多了,就变成了我们熟悉的类似类与类之间的循环引用一样,所以我们需要添加关键字unowned,如下:

1
2
3
4
5
6
7
if true {
var person = Person()
person.function = {[unowned person] in print("My name is \(person.name)")}
person.function()
}
//控制台打印“My name is Peter”
//同时控制台也打印“Person deinit”,person成功释放

其实很简单,就和类与类之间的循环引用一样,我们只需要打破这个引用环即可。不过这边使用unowned是因为被捕获的person变量对象和这个闭包的生命周期是一致的,所以用unowned,如果在这个closure的生命周期中,这个被捕获的person对象有可能为nil,那么就需要使用weak关键字来修饰了。这个很重要,如果用错有时会发生意想不到的错误。

总结

闭包在swift中用的非常多,特别是如果我们以函数式思想来设计一些代码,或多或少我们会遇到一些情况会发生循环引用,如果我们能够清楚闭包如何捕获变量,清楚捕获列表如何捕获变量,那么我们出现循环引用bug的机率就会小很多,希望上面的相关只是能够帮到你。

Author: Uncle Peter
Link: http://yoursite.com/2017/06/09/swift中closure捕获列表的研究/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.