Ленивая инициализация и сохранение цикла

Есть ли вероятность сохранения циклов при использовании ленивых инициализаторов?

В сообщении в блоге и многих других местах [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)
//..

И обнаружил, что «человек deinit» был зарегистрирован.

Таким образом, кажется, что нет циклов сохранения. Насколько мне известно, когда блок захватывает себя и когда этот блок сильно удерживается собой, возникает цикл удержания. Этот случай кажется похожим на цикл сохранения, но на самом деле это не так.


person BangOperator    schedule 01.07.2016    source источник
comment
Вы пробовали? Добавьте метод deinit и проверьте, вызывается ли он, когда вы ожидаете, что объект будет освобожден. Или используйте инструменты отладки памяти в Xcode/Instruments.   -  person Martin R    schedule 01.07.2016
comment
когда вы используете блоки или замыкания, вы можете случайно создать сильные циклы удержания — это не имеет ничего общего с lazy инициализаторами.   -  person holex    schedule 01.07.2016
comment
привет @MartinR deinit был вызван даже без списка захвата.   -  person BangOperator    schedule 01.07.2016
comment
@holex кажется, что управление памятью блоков отличается, когда речь идет о ленивых свойствах. Как указано в ответе, закрытие ленивых свойств неявно не экранируется. И это меняет правила управления памятью для таких замыканий.   -  person BangOperator    schedule 01.07.2016


Ответы (2)


Я попробовал это [...]

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

кажется, что нет циклов сохранения

Правильный.

Причина в том, что немедленно примененное замыкание {}() считается @noescape. Он не сохраняет захваченные self.

Для справки: твит Джо Гроффа.

person Nikolai Ruhe    schedule 01.07.2016
comment
Другой способ думать об этом состоит в том, что компилятор может безопасно решить не применять ARC для себя в закрытии отложенной переменной, потому что замыкание может быть вызвано только кодом, который все еще сохраняет экземпляр класса (в этом примере экземпляр Person). Так что нет необходимости в другом уровне сохранения экземпляра (также известного как «я»). Мне также понравилась ссылка @noescape в этом ответе. - person WeakPointer; 03.09.2017

В этом случае вам не нужен список захвата, так как ссылка self не принадлежит после создания экземпляра personalizedGreeting.

Как пишет MartinR в своем комментарии, вы можете легко проверить свою гипотезу, зарегистрировав, был ли объект Person деинициализирован или нет, когда вы удаляете список захвата.

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!

Очевидно, что в этом случае нет риска сильного цикла ссылок и, следовательно, нет необходимости в списке захвата unowned self в ленивом закрытии. Причина этого в том, что ленивое замыкание выполняется только один раз и использует только возвращаемое значение замыкания для (ленивого) создания экземпляра 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

Гипотеза: ленивое создание экземпляров замыканий автоматически фиксирует 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

То есть, f в foo() не деинициализируется, и, учитывая этот цикл строгой ссылки, мы можем сделать вывод, что self сильно захвачено при создании экземпляра закрытия ленивой переменной dummy.

Мы также можем видеть, что мы никогда не создаем цикл строгой ссылки в случае, если мы никогда не создаем экземпляр dummy, который будет поддерживать то, что ленивое создание экземпляра не более одного раза можно рассматривать как область времени выполнения (во многом похожую на никогда не достигаемое если), то есть либо а) никогда не достигался (не инициализировался), либо б) достигался, полностью выполнялся и "выбрасывался" (конец области действия).

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

Дополнительную информацию о циклах с сильными ссылками см., например, в

person dfrib    schedule 01.07.2016
comment
Очевидно, что в этом случае нет риска сильного эталонного цикла: Ну, по крайней мере, для меня это не очевидно. Если к ленивому свойству никогда не будет доступа, закрытие инициализации останется навсегда. Почему он не удерживает экземпляр от освобождения? Есть ли какое-то волшебство в замыканиях с ленивой инициализацией, всегда интерпретирующих ссылки на self как weak? - person Nikolai Ruhe; 01.07.2016
comment
Я имел в виду кажущийся экспериментальным (возможно, кажущийся был неудачным выбором слов). В любом случае, personalizedGreeting сам по себе является просто типом значения (String), он сам по себе не может содержать ссылку на self. Замыкание, выполняемое не более чем один раз на лету, используемое для (возможно) создания экземпляра p...Greeting, не является самим объектом, поэтому оно не может содержать ссылки на self. Он выполняется только один раз, если мы запрашиваем создание экземпляра p...Greeting. Если мы никогда не получим доступ к p...Greeting, этот неэкземплярный тип значения будет освобожден вместе с объектом класса, когда он выйдет за рамки. - person dfrib; 01.07.2016
comment
Я согласен с тем, что вы говорите, но это не отвечает на вопрос. В вашем примере закрытие ссылается на self. Если это сильная ссылка, она будет поддерживать экземпляр в живых до тех пор, пока закрытие не будет освобождено (что не может произойти до инициализации свойства). Таким образом, единственное объяснение, которое я могу придумать, это: замыкания в ленивой инициализации свойств автоматически всегда захватывают self слабо (или, что более вероятно, unowned). Это имело бы смысл и объясняло бы наблюдаемое поведение отсутствующего эталонного цикла. - person Nikolai Ruhe; 01.07.2016
comment
@NikolaiRuhe Я вернусь к этому после обеда, когда вернусь в офис. Моя теория заключается в том, что либо 1. это так, как вы описываете (weak захват по умолчанию), либо 2. ленивое закрытие экземпляра не более одного раза можно рассматривать как область выполнения (во многом похожую на никогда не достигаемую if), которая либо а) никогда не достигнуто (не инициализировано) или б) достигнуто, полностью выполнено и выброшено (конец области видимости), где результатом последнего является только возвращаемый тип замыкания, который в данном случае является просто типом значения. - person dfrib; 01.07.2016
comment
Я не смог найти документацию, подтверждающую теорию авто-слабости. Кроме того, мой краткий просмотр исходного кода Swift не дал намёка. В любом случае, дальнейшее тестирование, похоже, подтверждает теорию о том, что ленивые инициализаторы являются нормальными замыканиями, за исключением того, что self не считается строгой ссылкой. - person Nikolai Ruhe; 01.07.2016
comment
Решено. Смотрите мой ответ. - person Nikolai Ruhe; 01.07.2016
comment
@NikolaiRuhe А, хорошо. Я также только что добавил эксперимент, отбрасывающий эталонную гипотезу по умолчанию weak (или unowned), но, похоже, я опоздал на 1 минуту :) - person dfrib; 01.07.2016
comment
Извините, но я не могу понять ваш вывод. Test1 просто показывает, что обычный эталонный цикл (Foo.bar -> Bar.foo -> cycle) работает как положено. Закрытие и ленивая инициализация выполняются и освобождаются к моменту выполнения цикла. И Test2 ничего не добавляет к исходной настройке. - person Nikolai Ruhe; 01.07.2016
comment
С другой стороны, мои тесты показали, что замыкание является нормальным замыканием захвата, которое действительно удерживает значения, которые оно захватывает, за исключением self. Вы можете попробовать это, сделав bar глобальной переменной и зафиксировав ее в замыкании. Затем вы увидите, что Bar высвобождается точно при первом доступе к ленивому свойству. - person Nikolai Ruhe; 01.07.2016
comment
@NikolaiRuhe Вы указали выше Итак, единственное объяснение, которое я могу придумать, это: замыкания в ленивой инициализации свойств автоматически всегда слабо захватывают себя (или, что более вероятно, не принадлежат), и это была гипотеза, которую я хотел исследовать выше (т. е. показать, что замыкание является просто нормальным сильно захватывающим замыканием). Вы также написали Если это сильная ссылка, экземпляр останется живым до тех пор, пока замыкание не будет освобождено: во втором тесте я хотел проверить свою гипотезу о том, что выполненное закрытие на самом деле никогда не действует, пока оно не будет выполнено. - person dfrib; 01.07.2016
comment
... возможно, сильный цикл в тесте 1, однако, не зависит от того, как захвачен self, но тест 2, с другой стороны, показывает, по крайней мере, что если мы никогда не создадим экземпляр ленивой переменной, то закрытие никогда не будет действовать ( и не нужно освобождать). Однако в то же время, когда я закончил редактирование этого сообщения, вы опубликовали свое решение относительно @noescape, поэтому все это стало немного неуместным. Может быть, я удалю этот ответ, я просмотрю его позже и посмотрю, не имеет ли что-нибудь ценность. - person dfrib; 01.07.2016
comment
Но спасибо за интересную дискуссию :) - person Nikolai Ruhe; 01.07.2016
comment
@NikolaiRuhe Так же! - person dfrib; 01.07.2016