17.4 闭包中的强引用循环

由于闭包本质上也是引用类型,因此也可能在闭包和上下文捕获变量(或常量)之间出现强引用循环问题。

并不是所有的捕获变量(或常量)都会发生强引用循环问题,只有将一个闭包赋值给对象的某个属性,并且这个闭包体使用了该对象,才会产生闭包强引用循环。

17.4.1 一个闭包中的强引用循环示例

我们通过一个示例来了解一下闭包中的强引用循环。示例代码如下:

  1. class Employee {
  2. var no : Int = 0
  3. var firstName : String
  4. var lastName : String
  5. var job : String
  6. var salary : Double
  7. init(no : Int, firstName : String, lastName : String, job : String, salary : Double) {
  8. self.no = no
  9. self.firstName = firstName
  10. self.lastName = lastName
  11. self.job = job
  12. self.salary = salary
  13. println("员工\(firstName) 已经构造成功。")
  14. }
  15. deinit {
  16. println("员工\(firstName) 已经析构成功。")
  17. }
  18. lazy var fullName : ()-> String = {
  19. return self.firstName + "." + self.lastName
  20. }
  21. }
  22. var emp : Employee? = Employee(no: 7698, firstName: "Tony", lastName : "Guan",
  23. job :"Salesman", salary : 1600)
  24. println(emp!.fullName())
  25. emp = nil

上述代码第①行定义了类Employee,第②行代码定义了计算属性fullName,这个属性值的计算是通过一个闭包实现的,闭包的返回值类型是()-> String。属性fullName被声明为lazy,说明该属性是延迟加载的,由于在第③行代码中捕获了selfself能够在闭包体中使用,那么属性必须声明为lazy,即所有属性初始化完成后,self表示的对象才能被创建。

第④行代码emp!.fullName()调用闭包返回fullName属性值。代码第⑤行emp = nil断开强引用,释放对象。程序输出的结果是:

  1. 员工Tony 已经构造成功。
  2. Tony.Guan

从结果可见,析构器并没有被调用,也就是说对象没有被释放。导致这个问题的原因是闭包与捕获对象之间发生了强引用循环。

17.4.2 解决闭包强引用循环

解决闭包强引用循环问题有两种方法:弱引用和无主引用。到底应该采用弱引用还是无主引用,与两个对象之间的选择条件是一样的。如果闭包和捕获的对象总是互相引用并且总是同时销毁,则将闭包内的捕获声明为无主引用。当捕获的对象有时可能为nil时,则将闭包内的捕获声明为弱引用。如果捕获的对象绝对不会为nil,那么应该采用无主引用。

Swift在闭包中定义了捕获列表来解决强引用循环问题,基本语法如下:

  1. lazy var 闭包: <闭包参数列表> -><返回值类型> = {
  2. [unowned 捕获对象] <闭包参数列表> -><返回值类型> in
  3. [weak 捕获对象] <闭包参数列表> -><返回值类型> in
  4. //闭包体
  5. }

  1. lazy var 闭包: () -> <返回值类型> = {
  2. [unowned 捕获对象] in
  3. [weak 捕获对象] in
  4. //闭包体
  5. }

上述语法格式可以定义两种闭包捕获列表,其中第①行代码的语法格式是最为普通的格式,其中<闭包参数列表> -><返回值类型>与第②行和第③行的<闭包参数列表> -><返回值类型>要对应上。示例如下:

  1. lazy var fullName : (String, String) -> String = {
  2. [weakself] (firstName : String, lastName : String) -> String in
  3. //闭包体
  4. }

第④行的语法格式是无参数情况下的捕获列表,可以省略参数类别,只保留in,Swift编译器会通过上下文推断出参数列表和返回值类型。示例如下:

  1. lazy var fullName : () -> String = {
  2. [unownedself] in
  3. //闭包体
  4. }

下面看示例代码:

  1. class Employee {
  2. var no : Int = 0
  3. var firstName : String
  4. var lastName : String
  5. var job : String
  6. var salary : Double
  7. init(no : Int, firstName : String, lastName : String, job : String, salary : Double) {
  8. self.no = no
  9. self.firstName = firstName
  10. self.lastName = lastName
  11. self.job = job
  12. self.salary = salary
  13. println("员工\(firstName) 已经构造成功。")
  14. }
  15. deinit {
  16. println("员工\(firstName) 已经析构成功。")
  17. }
  18. lazy var fullName : ()-> String = {
  19. [weak self] ()-> String in
  20. letfn = self!.firstName
  21. letln = self!.lastName
  22. returnfn + "." + ln
  23. }
  24. }
  25. var emp : Employee? = Employee(no: 7698, firstName: "Tony", lastName : "Guan",
  26. job :"Salesman", salary : 1600)
  27. println(emp!.fullName())
  28. emp = nil

我们将第①行代码修改为[weak self] ()-> String in,该捕获列表是弱引用,捕获对象是self。由于是弱引用,在引用self的时候,代码第②行和第③行需要在后面加感叹号“!”,表明是强制拆封。

程序输出的结果是:

  1. 员工Tony 已经构造成功。
  2. Tony.Guan
  3. 员工Tony 已经析构成功。

从结果可见析构器被调用了。

我们可以将fullName属性定义为无主引用的捕获列表形式,代码如下:

  1. lazy var fullName : ()-> String = {
  2. [unownedself] in
  3. letfn = self.firstName
  4. letln = self.lastName
  5. returnfn + "." + ln
  6. }

采用哪一种引用方式,可以根据用户需求而定,这里不再赘述。此外,我们还可以根据需要为闭包提供参数。