The (Not-So) Peculiar Case of Function Retain in Swift

Consider the following Swift code:

class A {
    let action: () -> Void = {
        print("hello world")
    }
    
    deinit {
        print("A is deinited")
    }
    
    let b = B()

    init() {
        b.action = action
    }
}

class B {
    var action: (() -> Void)?
    
    deinit {
        print("B is deinited")
    }
}

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

// [Console Output]
// A is deinited
// B is deinited

This code may look fine—because it is. However, what if we make the following change?

class A {
    deinit {
        print("A is deinited")
    }
    
    let b = B()

    init() {
        b.action = action
    }
    
    func action() -> Void {
        print("hello world")
    }
}

class B {
    var action: (() -> Void)?
    
    deinit {
        print("B is deinited")
    }
}

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

// [Console Output]
// (nothing)

Okay, so the action of class A is changed from an instance variable to an instance method. It should be fine, right? However, the problem is: it isn’t. If you exec the code, you will find the execution result different from the former one. There is a clear retain cycle—when a is set to nil, both instances are not released.

But why is it the case?

Now let’s complicate the things a little bit to illustrate.

To complicate the thing

Now let’s put an instance variable of a counter into class A, and we increment the variable in action.

class A {
    var counter: Int = 0
    
    lazy var action: () -> Void = {
        self.counter += 1
    }
    
    deinit {
        print("A is deinited")
    }
    
    let b = B()

    init() {
        b.action = action
    }
}

// ...
class A {
    var counter: Int = 0

    deinit {
        print("A is deinited")
    }
    
    let b = B()

    init() {
        b.action = action
    }
    
    func action() -> Void {
        counter += 1
    }
}

// ...

With the two implementations above, we can already see some difference. The implementation using a closure would ask us to explicitly refer to self as it captures it. But why don’t we need to do that it the method version? Well, because an instance method by definition always implicitly captures self.

If you don’t see why it is retaining self (I don’t see any self here?), maybe thinking of it this way can help:

Any instance method can be written as ClassName.methodName(instanceName). For instance, the following two method calls are identical, except that one line uses optional-chaining while the the other line uses a forced-unwrap:

// ...

var a: A? = A()
a?.action()
A.action(a!)()

So when passing an action to the instance variable of B with action, we are basically writing A.action(self):

class A {
    var counter: Int = 0
    
    deinit {
        print("A is deinited")
    }
    
    let b = B()

    init() {
        b.action = A.action(self)
    }
    
    func action() -> Void {
        counter += 1
    }
}

class B {
    var action: (() -> Void)?
    
    deinit {
        print("B is deinited")
    }
}

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

// [Console Output]
// (nothing)

If you are not convinced yet

Okay, in case that didn’t help, let’s discuss further.

While it would be obvious in the first place when the retain cycle happened, let’s show it with another piece of code. So what is the behaviour when an instance is not released? While, it lives longer in memory when the reference we have is already away.

With a simpler example:

class MyClass {
    var printLine: ((_ line: String) -> Void)?
}

class Printer {
    var printedLines: [String] = []
    func printLine(line: String) {
        printedLines.append(line)
        print(printedLines)
    }
}

var instance: MyClass? = MyClass()
var printer: Printer? = Printer()
instance?.printLine = printer?.printLine
printer = nil

instance?.printLine?("Hello world")
instance?.printLine?("Goodbye world")

// [Console Output]
// ["Hello world"]
// ["Hello world", "Goodbye world"]

We can see we already lose access to the printer when we set it to nil, but we can still use the instance through the printLine variable in the instance.

Consider we use a closure and we weak the reference to self.

class MyClass {
    var printLine: ((_ line: String) -> Void)?
}

class Printer {
    var printedLines: [String] = []
    lazy var printLine: (String) -> Void = { [weak self] line in
        self?.printedLines.append(line)
        print(self?.printedLines ?? [])
    }
}

var instance: MyClass? = MyClass()
var printer: Printer? = Printer()
instance?.printLine = printer?.printLine
printer = nil

instance?.printLine?("Hello world")
instance?.printLine?("Goodbye world")

// [Console Output]
// []
// []

What a big surprise! As the closure does not retain self, when the printer reference is removed, there is no reference left, the instance of printer would be deinitialised.

The Takeaway

An instance method always implicitly captures self, so NEVER assign it as closure to another class’s instance, especially when self owns the instance as it would cause a retain cycle.