惰性初始化和保留周期

Lazy initialisation and retain cycle

使用惰性初始化器时,是否有可能出现循环保留?

blog post 和许多其他地方 [unowned self] 可见

class Person {

    var name: String

    lazy var personalizedGreeting: String = {
        [unowned self] in
        return "Hello, \(self.name)!"
        }()

    init(name: String) {
        self.name = name
    }
}

我试过了

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")
    }
}

像这样使用

//...
let person = Person(name: "name")
print(person.personalizedGreeting)
//..

并且发现 "person deinit" 被记录了。

所以似乎没有保留循环。 据我所知,当一个块捕获 self 时,当这个块被 self 强烈保留时,就会有一个保留周期。这种情况看似类似于retain cycle,其实不然。

在这种情况下,您不需要捕获列表,因为在实例化 personalizedGreeting 之后没有引用 self

正如 MartinR 在他的评论中所写,您可以通过在删除捕获列表时记录 Person 对象是否被取消初始化来轻松检验您的假设。

例如

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!

显然在这种情况下不存在强引用循环的风险,因此在惰性闭包中不需要 unowned self 的捕获列表。这样做的原因是懒惰闭包只执行一次,并且只使用闭包的return值来(懒惰地)实例化personalizedGreeting,而在这种情况下,对 self 的引用不会超过闭包的执行。

但是,如果我们要在 Person 的 class 属性 中存储类似的闭包,我们将创建一个强引用循环,作为 属性 的self 将保持对 self 的强引用。例如:

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

假设:惰性实例化闭包自动将 self 捕获为 weak(或 unowned),默认情况下

当我们考虑下面的例子时,我们意识到这个假设是错误的。

/* 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

foo()中的f没有被反初始化,鉴于这个强引用循环,我们可以得出结论,self在惰性变量的实例化闭包中被强捕获dummy.

我们还可以看到,在我们从未实例化 dummy 的情况下,我们永远不会创建强引用循环,这将支持最多一次延迟实例化闭包可以被视为运行时范围(很像 never reached if) 要么 a) never reached (non-initialized) 要么 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!

有关强引用循环的更多阅读,请参阅

I tried this [...]

lazy var personalizedGreeting: String = { return self.name }()

it seems there are no retain cycles

正确。

原因是立即应用的闭包 {}() 被认为是 @noescape。它不保留捕获的 self.

供参考:Joe Groff's tweet.

在我的洋葱中,事情可能是这样的。该块肯定会捕获自引用。但是记住,如果做一个retain cycle,前提是self必须retain block。但如您所见,惰性 属性 仅保留块的 return 值。所以,如果惰性 属性 没有被初始化,那么外部上下文会保留惰性块,使其包含并且没有完成循环。但是有一件事我还是不清楚,就是惰性块何时被释放。如果 lazy 属性 被初始化,很明显 lazy 块很快就会被执行和释放,也没有 retain cycle 完成。我认为主要问题在于谁保留惰性块。如果在块内捕获 self 时没有完成保留周期,则它可能不是 self。至于@noescape,我不这么认为。 @noescape 不是不捕获,而是暂时存在,任何对象都不应该在这个块上有持久引用,或者换句话说,保留这个块。无法异步使用该块 。如果@noescape 是事实,惰性块如何持续到惰性 属性 被初始化?