'Lazy initialisation and retain cycle
While using lazy initialisers, is there a chance of having retain cycles?
In a blog post and many other places [unowned self]
is seen
class Person {
var name: String
lazy var personalizedGreeting: String = {
[unowned self] in
return "Hello, \(self.name)!"
}()
init(name: String) {
self.name = name
}
}
I tried this
class Person {
var name: String
lazy var personalizedGreeting: String = {
//[unowned self] in
return "Hello, \(self.name)!"
}()
init(name: String) {
print("person init")
self.name = name
}
deinit {
print("person deinit")
}
}
Used it like this
//...
let person = Person(name: "name")
print(person.personalizedGreeting)
//..
And found that "person deinit" was logged.
So it seems there are no retain cycles. As per my knowledge when a block captures self and when this block is strongly retained by self, there is a retain cycle. This case seems similar to a retain cycle but actually it is not.
Solution 1:[1]
I tried this [...]
lazy var personalizedGreeting: String = { return self.name }()
it seems there are no retain cycles
Correct.
The reason is that the immediately applied closure {}()
is considered @noescape
. It does not retain the captured self
.
For reference: Joe Groff's tweet.
Solution 2:[2]
In this case, you need no capture list as no reference self
is pertained after instantiation of personalizedGreeting
.
As MartinR writes in his comment, you can easily test out your hypothesis by logging whether a Person
object is deinitilized or not when you remove the capture list.
E.g.
class Person {
var name: String
lazy var personalizedGreeting: String = {
_ in
return "Hello, \(self.name)!"
}()
init(name: String) {
self.name = name
}
deinit { print("deinitialized!") }
}
func foo() {
let p = Person(name: "Foo")
print(p.personalizedGreeting) // Hello Foo!
}
foo() // deinitialized!
It is apparent that there is no risk of a strong reference cycle in this case, and hence, no need for the capture list of unowned self
in the lazy closure. The reason for this is that the lazy closure only only executes once, and only use the return value of the closure to (lazily) instantiate personalizedGreeting
, whereas the reference to self
does not, in this case, outlive the execution of the closure.
If we were to store a similar closure in a class property of Person
, however, we would create a strong reference cycle, as a property of self
would keep a strong reference back to self
. E.g.:
class Person {
var name: String
var personalizedGreeting: (() -> String)?
init(name: String) {
self.name = name
personalizedGreeting = {
() -> String in return "Hello, \(self.name)!"
}
}
deinit { print("deinitialized!") }
}
func foo() {
let p = Person(name: "Foo")
}
foo() // ... nothing : strong reference cycle
Hypothesis: lazy instantiating closures automatically captures self
as weak
(or unowned
), by default
As we consider the following example, we realize that this hypothesis is wrong.
/* Test 1: execute lazy instantiation closure */
class Bar {
var foo: Foo? = nil
}
class Foo {
let bar = Bar()
lazy var dummy: String = {
_ in
print("executed")
self.bar.foo = self
/* if self is captured as strong, the deinit
will never be reached, given that this
closure is executed */
return "dummy"
}()
deinit { print("deinitialized!") }
}
func foo() {
let f = Foo()
// Test 1: execute closure
print(f.dummy) // executed, dummy
}
foo() // ... nothing: strong reference cycle
I.e., f
in foo()
is not deinitialized, and given this strong reference cycle we can draw the conclusion that self
is captured strongly in the instantiating closure of the lazy variable dummy
.
We can also see that we never create the strong reference cycle in case we never instantiate dummy
, which would support that the at-most-once lazy instantiating closure can be seen as a runtime-scope (much like a never reached if) that is either a) never reached (non-initialized) or b) reached, fully executed and "thrown away" (end of scope).
/* Test 2: don't execute lazy instantiation closure */
class Bar {
var foo: Foo? = nil
}
class Foo {
let bar = Bar()
lazy var dummy: String = {
_ in
print("executed")
self.bar.foo = self
return "dummy"
}()
deinit { print("deinitialized!") }
}
func foo() {
let p = Foo()
// Test 2: don't execute closure
// print(p.dummy)
}
foo() // deinitialized!
For additional reading on strong reference cycles, see e.g.
Solution 3:[3]
In my onion, things may work like this. The block surely capture the self reference. But remember, if a retain cycle is done, the precondition is that the self must retain the block. But as you could see, the lazy property only retains the return value of the block. So, if the lazy property is not initialized, then the outside context retains the lazy block, make it consist and no retain cycle is done. But there is one thing that I still don't clear, when the lazy block gets released. If the lazy property is initialized, it is obvious that the lazy block get executed and released soon after that, also no retain cycle is done. The main problem lies on who retains the lazy block, I think. It's probably not the self, if no retain cycle is done when you capture self inside the block. As to @noescape, I don't think so. @noescape doesn't mean no capture, but instead, means temporary existence, and no objects should have persistent reference on this block, or in an other word, retain this block. The block could not be used asynchronously see this topic. If @noescape is the fact, how could the lazy block persist until the lazy property get initialized?
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Vadim |
Solution 2 | |
Solution 3 | Neal.Marlin |