35

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.

mfaani
  • 33,269
  • 19
  • 164
  • 293
BangOperator
  • 4,377
  • 2
  • 24
  • 38
  • 2
    Did you try it? Add a `deinit` method and check if it is called when you expect the object to be deallocated. Or use the memory debugging tools in Xcode/Instruments. – Martin R Jul 01 '16 at 09:05
  • when you use _blocks_ or _closures_ you can accidentally create strong retain cycles – it is nothing to do with `lazy` initialisers. – holex Jul 01 '16 at 09:19
  • hello @MartinR deinit was called even without capture list. – BangOperator Jul 01 '16 at 10:24
  • @holex it seems blocks memory management differs when it comes to lazy properties. As pointed in the answer, closures to lazy properties are implicitly noescaping. And this changes the memory management rules for such closures. – BangOperator Jul 01 '16 at 12:14

3 Answers3

73

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.

Vadim
  • 9,383
  • 7
  • 36
  • 58
Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • 1
    Another way to think about this is that the compiler can safely decide not to apply ARC for self in the lazy var's closure because the closure could only be invoked by code that still was retaining the class instance anyway (in this example a Person instance). So no need with another level of retain on the instance (aka self). I also liked the @noescape reference in this answer. – WeakPointer Sep 03 '17 at 14:58
6

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.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • 1
    "It is apparent that there is no risk of a strong reference cycle in this case": Well, at least to me this is not apparent. If the lazy property is never accessed the initialization closure would stay forever. Why is it not keeping the instance from deallocating? Is there some magic in lazy initialization closures, always interpreting references to `self` as `weak`? – Nikolai Ruhe Jul 01 '16 at 09:39
  • I was referring to apparent by experiment (perhaps appparent was a bad choice of words). Anyway, `personalizedGreeting` itself is just a simple value type (`String`), it can't by itself hold a reference to `self`. The at-most-on-the-fly-executed-once closure used for (possibly) instantiating `p...Greeting` is not an object itself, so it can't hold references to `self`. It is only _executed once_ if we're asking for `p...Greeting` to be instantiated. If we never never access `p...Greeting`, this non-instantiated value type will be deallocated along with the class object when out of scope. – dfrib Jul 01 '16 at 09:53
  • 2
    I agree with what you say but it does not answer the question. In your example, the closure takes a reference to `self`. If that is a strong reference it would keep the instance alive as long as the closure is not deallocated (which can't happen before the property is initialized). So the only explanation that I can think of is: Closures in lazy property initialization automatically always capture `self` weakly (or, more likely, `unowned`). This would totally make sense and explain the observed behavior of the "missing" reference cycle. – Nikolai Ruhe Jul 01 '16 at 10:06
  • @NikolaiRuhe I'll look back into this after lunch when I'm back at the office. My theory that either 1. it is as you describe (`weak` capturing as per default), or 2. 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), where the result of the latter is only the return type of the closure, which in this case is just a value type. – dfrib Jul 01 '16 at 10:17
  • I couldn't find documentation backing the auto-weak theory. Also my brief browsing of the swift source code did not reveal a hint. Anyway, further testing seems to support the theory that lazy initializers are normal closures with the exception of `self` not being considered a strong reference. – Nikolai Ruhe Jul 01 '16 at 10:52
  • Solved. See my answer. – Nikolai Ruhe Jul 01 '16 at 11:23
  • @NikolaiRuhe Ah, nice. I also just added an experiment throwing away the default `weak` (or `unowned`) reference hypothesis, but seems I was 1 minute too late :) – dfrib Jul 01 '16 at 11:28
  • Sorry, but I can't follow your conclusion. Test1 just shows that a normal reference cycle (Foo.bar -> Bar.foo -> cycle) works as expected. The closure and lazy initialization is done and released by the time the cycle is in place. And Test2 does not add anything to the original setup. – Nikolai Ruhe Jul 01 '16 at 12:30
  • On the other hand my tests showed that the closure is a normal capturing closure, that *does* hold on to the values it captures, with the exception of `self`. You can try that out by making bar a global variable and capture this in the closure. You will then see that `Bar` is released exactly when the lazy property is accessed for the first time. – Nikolai Ruhe Jul 01 '16 at 12:31
  • @NikolaiRuhe You stated above _"So the only explanation that I can think of is: Closures in lazy property initialization automatically always capture self weakly (or, more likely, unowned)"_ and that was the hypothesis that I wanted to investigate above (i.e., show that the closure is just a normal strongly capturing closure). You also wrote _"If that is a strong reference it would keep the instance alive **as long as the closure is not deallocated**"_: with the 2nd test I wanted to investigate my hypothesis that the executed closure is in fact never in effect at all until it is executed. – dfrib Jul 01 '16 at 12:45
  • ... possibly the strong cycle in test 1 is, however, independent of how `self` is captured, but test 2, on the other hand, shows at least that if we never instantiate the lazy var then the closure will never be in effect (and have no need to be deallocate). At the same time as I finished this post edit, however, you posted your solution regarding `@noescape`, so it all became a bit non-relevant. Maybe I'll remove this answer, I'll look over it later and see if anything left is of value. – dfrib Jul 01 '16 at 12:47
  • Thanks for the interesting discussion, though :) – Nikolai Ruhe Jul 01 '16 at 12:49
  • @NikolaiRuhe Likewise! – dfrib Jul 01 '16 at 12:50
0

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?

Neal.Marlin
  • 494
  • 5
  • 17