web-dev-qa-db-ja.com

遅延初期化と保持サイクル

遅延初期化子を使用しているときに、保持サイクルが発生する可能性はありますか?

ブログ投稿 と他の多くの場所[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」がログに記録されていることがわかりました。

したがって、保持サイクルはないようです。ブロックが自己をキャプチャするとき、およびこのブロックが自己によって強く保持されるときの私の知識によれば、保持サイクルがあります。このケースは保持サイクルに似ていますが、実際はそうではありません。

22
BangOperator

私はこれを試しました[...]

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

保持サイクルがないようです

正しい。

その理由は、即座に適用されたクロージャ{}()が_@noescape_と見なされるためです。キャプチャされたselfは保持されません。

参考: Joe Groffのツイート

61
Nikolai Ruhe

この場合、selfのインスタンス化後に参照personalizedGreetingが関係しないため、キャプチャリストは必要ありません。

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_のキャプチャリストは必要ありません。この理由は、遅延クロージャはonceのみを実行し、クロージャの戻り値のみを使用して(ゆっくりと)personalizedGreetingをインスタンス化するためです。この場合、selfへの参照は、クロージャーの実行よりも長く存続しません。

ただし、Personのクラスプロパティに同様のクロージャーを格納する場合、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
_

仮説:遅延インスタンス化クロージャーは、デフォルトでselfweak(または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をインスタンス化しない場合は、強い参照サイクルを作成しないこともわかります。これにより、最大で1回の遅延インスタンス化クロージャをランタイムスコープと見なすことができます(到達しない場合と同様)。 a)到達しなかった(初期化されていない)、またはb)到達し、完全に実行され、「破棄された」(スコープの終わり)。

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

強参照サイクルの詳細については、例を参照してください。

4
dfri