Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why Swift closure not capture self?

I was testing swift closure with Xcode playground.

This is my code:

import UIKit

class A{
    var closure: ()->() = {}

    var name: String = "A"
    init() {
        self.closure = {
            self.name = self.name + " Plus"
        }
    }

    deinit {
        print(name + " is deinit")
    }
}

var a: A?
a = A()
a = nil

As what is expected, a is self contained by closure, so a is never released.

But, when I add this line before the last line:

a?.closure = { a?.name = "ttt" }

Then, I found "A is deinit" in the output window, which means a is released. Why? is a not recycle reference?

To be test, I use a function to set the closure, which the code is version 2:

import UIKit

class A{
    var closure: ()->() = {}
    func funcToSetClosure(){
        self.closure = { self.name = "BBB"}
    }
    var name: String = "A"
    init() {
        self.closure = {
            self.name = self.name + " Plus"
        }
    }

    deinit {
        print(name + " is deinit")
    }
}



var a: A?


a = A()


a?.funcToSetClosure()


a = nil

Again, a is never released.

So I got the conclusion, when closure is set by init or a function in the class, it will cause recycle reference, when it is set out side the class, it will not cause recycle reference. Am I right?

like image 808
Jungen yan Avatar asked Dec 05 '16 16:12

Jungen yan


3 Answers

There are retain cycles in both cases. The difference is the nature of the reference, not the place where closure is set. This difference is manifested in what it takes to break the cycle:

  • In the "inside" situation, the reference inside the closure is self. When you release your reference to a, that is insufficient to break the cycle, because the cycle is directly self-referential. To break the cycle, you would have had also to set a.closure to nil before setting a to nil, and you didn't do that.

enter image description here

  • In the "outside" situation, the reference is a. There is a retain cycle so long as your a reference is not set to nil. But you eventually do set it to nil, which is sufficient to break the cycle.

enter image description here

(Illustrations come from Xcode's memory graph feature. So cool.)

like image 113
matt Avatar answered Oct 06 '22 12:10

matt


As the SIL documentation says, when you capture a local variable in a closure, it will be stored on the heap with reference counting:

Captured local variables and the payloads of indirect value types are stored on the heap. The type @box T is a reference-counted type that references a box containing a mutable value of type T.

Therefore when you say:

var a : A? = A()
a?.closure = { a?.name = "ttt" }

you do have a reference cycle (which you can easily verify). This is because the instance of A has a reference to the closure property, which has a reference to the heap-allocated boxed A? instance (due to the fact that it's being captured by the closure), which in turn has a reference to the instance of A.

However, you then say:

a = nil

Which sets the heap-allocated boxed A? instance's value to .none, thus releasing its reference to the instance of A, therefore meaning that you no longer have a reference cycle, and thus A can be deallocated.

Just letting a fall out of scope without assigning a = nil will not break the reference cycle, as the instance of A? on the heap is still being retained by the closure property of A, which is still being retained by the A? instance.

like image 3
Hamish Avatar answered Oct 06 '22 10:10

Hamish


What causes the retain cycle is that you reference self in the closure.

var a: A?
a = A()
a?.closure = { a?.name = "ttt" }
a = nil

You change the closure to no longer reference self, that's why it is deallocated.

In the final example, you make it reference self again in the closure, that is why it does not deallocate. There are ways around this, this post is a great list of when to use each case in swift: How to Correctly handle Weak Self in Swift Blocks with Arguments

I would imagine you are looking for something like this, where you use a weak reference to self inside the block. Swift has some new ways to do this, most commonly using the [unowned self] notation at the front of the block.

init() {
    self.closure = { [unowned self] in
        self.name = self.name + " Plus"
    }
}

More reading on what is going on here: Shall we always use [unowned self] inside closure in Swift

like image 2
Alex Avatar answered Oct 06 '22 10:10

Alex