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.
Is it not considered a reference increment when the closure captures the instance (which stores the closure as a property) as a variable?
Does Swift automatically attach a weak or unowned reference to the capture list if the reference isn't self?
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.
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".
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.
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.
(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)
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 headingby 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.
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).