Swift Closure - ARC

The code below creates a strong reference cycle.


class Car {

    let doors = 10

    lazy var closure : () -> Int = {
  
        return 55
    }

    deinit {
        print("de-initialized")
    }

}

func f1() {

    print("f1")

    var c1 : Car? = Car()

    c1?.closure = {
  
        return (c1?.doors ?? 99) + 1
    }

    //c1 = nil
}

f1()


Questions:

  1. Why is the instance of Car not de-initialized though c1 goes out of scope ?
  2. However when c1 is assigned nil, Car instance is de-initialized, why is that ?
  3. In the code, c1?.closure is assigned a closure, but the closure is not invoked, so why does it create a strong reference cycle ?

Accepted Reply

>> why is there a strong reference established when the closure is just assigned (line 22)


Because the assignment creates all of the additional references: from the c1!.closure property to the closure, and from the closure to c1. (The third reference is the existing one from c1.some to the Car instance.)


It's worth pointing out that capturing c1 in the closure causes c1 to be copied from the stack to the heap. In effect, although c1 is a value type (a kind of enum, that is), copying it to the heap makes it a reference type (or, maybe a better way to say it?, wraps it in a reference type).


>> In Code 2, strong reference is established only when closure is invoked, otherwise there is no strong reference cycle


But not really because the closure is invoked. The real cycle is in the "self.doors" reference in the closure. It just doesn't get "activated" until the lazy property is evaluated. So, for example, if the closure is just referenced (but not invoked) from outside the Car class, the reference cycle will still appear. Try this instead of your line 22:


    _ = c1?.closure // removed the () to invoke the closure


Still no dealloc.

Replies

if you declare


weak var c1 : Car? = Car()


deinit should be called. It is not called because a strong reference is kept so ARC cannot deallocate because it is not set to nil.


see: h ttps://krakendev.io/blog/weak-and-unowned-references-in-swift

or this one:

h ttps://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html


When closure is created, there must be a copy of instance somewhere, hence the reference cycle.


Buts experts in ARC will certainly give a much more precise explanation.


Note : il you want to print the name of the func, you can use print(#function)

It behaves this way because it's a 3-way reference cycle, not a 2-way reference cycle. "c1" is an optional type, which means it's really a (generic) enum with two values: ".none" and ".some", with an associated value on the ".some" case. (The associated value is a reference to a Car object in this case.)


Since "c1" starts out non-nil, and the closure captures "c1" (not the unwrapped associated value), you have this cycle:


-> line 22 closure -> c1.some -> c1!.closure ->


Setting c1 explicitly to nil in line 27 changes the value of c1 from ".some" to ".none", which breaks the cycle.

So clear when you explain !!!😎

Some additional info: you could have avoided the strong reference cycle by using an appropriate capture list in the closure definition.


Either:


func f1() {
    print("f1")
    var c1 : Car? = Car()
    c1?.closure = { [weak c1] in
        return (c1?.doors ?? 99) + 1
    }
}


or, since there's no need for c1 to be an Optional in f1():


func f1() {
    print("f1")
    var c1 = Car()
    c1.closure = { [unowned c1] in
        return c1.doors + 1
    }
}

My Understanding:
closure
holds a strong reference to c1 and not to the instance c1 points to.


Scenario 1: When c1 goes out of scope:

- c1 is still being held by closure


c1 ⬅️ closure

↘️

Car ⤴️

Scenario 2: When c1 = nil:

c1 ⬅️ closure

⬇️

nil ⬆️

Car (since no one holds Car, it is deallocated)

Note:
- In original code, by merely declaring c1 an optional doesn't prevent a strong reference cycle, only when c1 is assigned nil it prevents a refernce cycle.

Question Remaining:

- In the original code, why is there a strong reference established when the closure is just assigned (line 22) and not invoked ?


- In Code 2, strong reference is established only when closure is invoked, otherwise there is no strong reference cycle.


Code 2:

class Car {

    let doors = 10

    lazy var closure : () -> Int = {

        return self.doors
    }

    deinit {
        print("de-initialized")
    }

}

func f1() {

    print("f1")

    let c1 : Car? = Car()

    _ = c1?.closure() /
}
f1()

>> why is there a strong reference established when the closure is just assigned (line 22)


Because the assignment creates all of the additional references: from the c1!.closure property to the closure, and from the closure to c1. (The third reference is the existing one from c1.some to the Car instance.)


It's worth pointing out that capturing c1 in the closure causes c1 to be copied from the stack to the heap. In effect, although c1 is a value type (a kind of enum, that is), copying it to the heap makes it a reference type (or, maybe a better way to say it?, wraps it in a reference type).


>> In Code 2, strong reference is established only when closure is invoked, otherwise there is no strong reference cycle


But not really because the closure is invoked. The real cycle is in the "self.doors" reference in the closure. It just doesn't get "activated" until the lazy property is evaluated. So, for example, if the closure is just referenced (but not invoked) from outside the Car class, the reference cycle will still appear. Try this instead of your line 22:


    _ = c1?.closure // removed the () to invoke the closure


Still no dealloc.

Thanks QuinceyMorris,


To summarise my understanding:

- closure is defined as a lazy property of the type () -> Int

- Since closure property was defined as lazy, the property is not assigned till the time the property is accessed.

- Accessing the property doesn't mean executing the closure, it only means accessing the closure property.

- As QuinceyMorris rightly pointed out, there is no need for the closure to be executed, once closure is assigned, the strong reference is established because the property type is () -> Int

- That explains why the assignment of a new closure caused a strong reference cycle while the lazy initialisation didn't have an effect till it was accessed.

Basically, yes.


In fact, both of your examples were sort of the same thing, because assigning the closure later is a kind of lazy initialization. Your first example had the extra complication of an optional creating an extra reference in the cycle. That was a very interesting complication!