How ARC does resolve Strong Reference Cycles for Closures that don't reference class self?

Hi Community!

I have a question about the Strong Reference Cycles for Closures.

The example below shows how you can create a strong reference cycle when using a closure that references self.

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

var heading: HTMLElement? = HTMLElement(name: "h1")
let defaultText = "some default text"
heading!.asHTML = {
    let wrappedHeading = heading!
    return "<\(wrappedHeading.name)>\(wrappedHeading.text ?? defaultText)</\(wrappedHeading.name)>"
}
print(heading!.asHTML())
// Prints "<h1>some default text</h1>"

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"

heading = nil
paragraph = nil
// Prints "h1 is being deinitialized"

In contrast with paragraph, heading is deallocated automatically which means that the reference cycle between heading and the new closure assigned to asHTML of heading didn't occur.

I was not able to find an appropriate answer to it in the official Swift document.

  1. Is it not considered a reference increment when the closure captures the instance (which stores the closure as a property) as a variable?
  2. Does Swift automatically attach a weak or unowned reference to the capture list if the reference isn't self?

You could find a note in Unowned Optional References that reads as follow:

The optional that wraps the class doesn’t use reference counting, so you don’t need to maintain a strong reference to the optional.

  1. Can it be the key to solving the problem of the example?
  2. What does the note exactly mean?

Thanks in advance!

When you assign heading!.asHTML to a closure in the top-level context, it is capturing the variable heading. When you later reassign heading to nil, you've manually broken the reference cycle by changing the reference chain from closure -> variable -> HTMLElement object -> closure to closure -> variable -> nil.

1 Like

Thank you for your opinion.

However, still I doubt the reference closure -> variable.
Please test the following code:

class Object {
    deinit {
        print("object is being deinitialized")
    }
}
var foo: Object? = Object()
var bar: Object? = foo
foo = nil
// Prints nothing

As you can see object is not deallocated, as both foo and bar have references to object respectively, even though you assigned foo to bar.

Now back to the original problem, references that I imagine look like:

variable --->|
             |-----> HTMLElement object ----->|
closure ---->|                                |
   ^                                          |
   |------------------------------------------|

closure captures variable - can be replaced into closure = variable literally.
As such, either closure refers to HTMLElement object, which is why I think it is a "cycle".

What do you think about it?
Thanks!

1 Like

Swift does not run cleanups on values that are still live at program exit, because the program exiting will clean up most resources automatically. Since bar still contains a reference to the object at the end of the program, its deinit won't run, but that's not because of a reference cycle.

5 Likes

The point of the previous example was that closure refers to HTMLElement object directly, not to variable.

I think as follows:
closure -> variable -> HTMLElement object -> closure
=>:
closure -> HTMLElement object -> closure
:thinking:

class Object {
	deinit {
		print("object is being deinitialized")
	}
}

do {
	var foo: Object? = Object()
	var bar: Object? = foo
	foo = nil
}

prints "object is being deinitialized"

1 Like

Let me simplify your example a bit:

class HTMLElement {
    var name: String = "name"
    var closure: () -> String = { "empty" }
    deinit {
        print("deinitialized")
    }
}

func foo() {
    let heading = HTMLElement()
    heading.closure = {
        heading.name
    }
}

foo()

Here you've got a reference cycle indeed and HTMLElement is not deinitialised.
You can fix it two ways:

  1. manually breaking the cycle when you are done with the closure:
    heading.closure = {""}
  1. capturing heading weakly:
    heading.closure = { [weak heading] in
        guard let heading else { return "" }
        return heading.name
    }
2 Likes

Thank you @tera !
It's very interesting. I understand your example.
But, would you be able to explain why you don't have to capture references in the following example?

class HTMLElement {
    var name: String = "name"
    var closure: () -> String = { "empty" }
    deinit {
        print("deinitialized")
    }
}

var heading: HTMLElement? = HTMLElement()
heading?.closure = {
    heading?.name ?? "empty"
}
heading?.closure()
heading = nil
// Prints "deinitialized"

The example runs exactly the same as your one, but not inside a function.

The closure does capture the variable heading. It captures the variable itself, not just the current value, so any changes you make to the variable also affect the variable within the closure. After you reassign heading to nil, there is no longer a reference cycle.

2 Likes

I believe optionals are captured as weak (or unowned?) by default, so when you assigned new closure to asHTML compiler broke the cycle for you

Ah, yes it is captured, as if you wrote:

{ [strong heading] in

or just

{ [heading] in

(I don't remember now if explicit strong is allowed or not)

You just don't have to write that explicitly because it happens implicitly anyway. It's only when you need to change from default "strong" to "weak" (or unowned, etc) capturing – only then you need to mention the variable explicitly in the capture list.

Nope, weak captures are implicitly Optional, but Optional captures are not implicitly weak (or unowned). You can use this test program to verify that the optional and non-optional captures have the same lifetime.

import Foundation

class Test {
  deinit {
    print("hello")
  }
}

func test() {
  var x:Test? = Test()
  var y:Test = Test()
  DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
    print("\(x)\(y)")
  }
}

test()
sleep(4)
1 Like

It makes sense to me.

Why do they not explain it in the official document explicitly about it? :thinking:

Yeah, I was thinking about optional closures being implicitly @escaping and somehow this transferred in my brainfart :D
Thanks for correcting me

1 Like

There is an important way in which explicit captures differ from implicit captures: implicit captures are by reference, explicit captures are by value:

var x = 0
var y = 0

let f = { [x] in
    print(x, y)
}

x = 1
y = 1

f() // 0 1

Notably, if you captured heading by value here then you will be unable to break the cycle by assigning heading to nil because the closure actually captures its own local copy of heading at the time the closure is formed.

2 Likes

It's getting more interesting.
So there are several different types of captures for closures.

  • weak
  • unowned
  • neither one above, just put in the list - [] without an attribute (what do you call it, btw?)
  • implicit (who knows if it is subcategorized into three of the above? i.e. implicit weak, implicit unowned, and implicit default - whatever?)

Please help me to find the link to the correct document or source code.

I found an answer to my question here:

myFunction { print(self.title) }                    // implicit strong capture
myFunction { [self] in print(self.title) }          // explicit strong capture
myFunction { [weak self] in print(self!.title) }    // weak capture
myFunction { [unowned self] in print(self.title) }  // unowned capture
1 Like

Very important difference, thanks for pointing out.

Yes. Plus unowned(safe) & unowned(unsafe) :slight_smile:
(I have no idea what those two mean and how are they different from "unowned").

unowned is unowned(safe), which reliably crashes if the object is used after being deallocated.

unowned(unsafe) behaves more like Objective-C, using it after being deallocated is undefined behavior (for example, a new object may be allocated in its place).

2 Likes