Skip to content

Instantly share code, notes, and snippets.

@ilyapuchka
Last active November 16, 2018 06:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ilyapuchka/8d0690101f23c6d09225 to your computer and use it in GitHub Desktop.
Save ilyapuchka/8d0690101f23c6d09225 to your computer and use it in GitHub Desktop.
class A {
let factory: ()->()
var closure: ((b: B)->())?
init(factory: ()->()) {
self.factory = factory
}
deinit {
print("deinit A")
}
}
class B {
var array = [A]()
func addA(factory: ()->()) -> A {
let a = A(factory: factory)
self.array.append(a)
return a
}
func call() {
for a in array {
a.factory()
a.closure?(b: self)
}
}
deinit {
print("deinit B")
}
}
var b: B! = B()
b.addA({
print("factory \(b)")
}).closure = { b in
print("closure \(b)")
}
b.addA({
print("factory \(b)")
}).closure = { b in
print("closure \(b)")
}
b.call()
print("should deinit")
b = nil
b
/*
Output:
factory B
closure B
factory B
closure B
should deinit
deinit B
deinit A
deinit A
*/
@ilyapuchka
Copy link
Author

B strongly reference to each of A's, each of them strongly reference it's factory and closure, factory strongly captures B from outer context - that should lead to retain cycle. A's closure though gets B via runtime argument, so it will create a reference only when that block is called and it will release this reference when block finishes executing, so that should not lead to retain cycle. But still there is no retain cycle at all, so [unowned self] in factory is not needed in factory block???

@AliSoftware
Copy link

For future reference here's the answer: https://twitter.com/jckarter/status/671812668005584896

@ilyapuchka
Copy link
Author

Indeed, capturing self and variable pointing the instance (but not self!) is not the same.

This will cause retain cycle:

func addA() -> A {
        let a = A(factory: {
            print("factory \(self)")
        })
        self.array.append(a)
        return a
    }

And this obviously too:

func addA() -> A {
        var b = self
        let a = A(factory: {
            print("factory \(b)")
        })
        b.array.append(a)
        return a
    }

@lilyball
Copy link

lilyball commented Dec 1, 2015

Note that if you used something like

do {
    let b = B()
    // ...
    print("should deinit")
    // b goes out of scope here
}
// did not deinit!

it actually would be a retain cycle. Which is to say, letting a variable go out of scope is different than explicitly assigning nil to it, when the variable has been captured.

@leonid-s-usov
Copy link

leonid-s-usov commented Nov 16, 2018

Hi @ilyapuchka, @AliSoftware

Got here from the playground of Dip

Sorry to resurrect this old topic, but @kballard is absolutely right. I have just confirmed that it all breaks (leaks) if the declaration is changed from an optional to a simple variable (or if the b = nil statement is removed)

So, there are two components to this behaviour:

  1. capturing an optional rather than a direct reference (doesn't depend on whether it's self or any other variable pointing to the same object; it only is a question of indirection which is present if it's an optional)
  2. manually breaking the retain cycle by setting the optional pointee to nil. This is effective for all cases of capturing this particular optional because it's captured by reference and not by value.

Which brings us back to the Dips statement that it's safe to capture the container inside a factory - this statement doesn't seem to be valid, and this gist is actually proving that the retain cycle does exist, and has to be addressed somehow by the code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment