Нормально ли, что lazy var свойство инициализируется дважды?

Я встретил очень странный случай использования свойства с ключевым словом lazy. Я знаю, что это ключевое слово указывает, что инициализация свойства отложена до тех пор, пока фактически не будет использована переменная, и просто запустится один раз.

Тем не менее, я нашел случай, запускающий инициализацию дважды.

class TestLazyViewController: UIViewController {

    var name: String = "" {
        didSet {
            NSLog("name self = \(self)")
            testLabel.text = name
        }
    }

    lazy var testLabel: UILabel = {
        NSLog("testLabel self = \(self)")
        let label = UILabel()
        label.text = "hello"
        self.view.addSubview(label)
        return label
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        testLabel.setTranslatesAutoresizingMaskIntoConstraints(false)
        NSLayoutConstraint.activateConstraints([NSLayoutConstraint(item: testLabel, attribute: .CenterX, relatedBy: .Equal, toItem: self.view, attribute: .CenterX, multiplier: 1.0, constant: 0.0)])
        NSLayoutConstraint.activateConstraints([NSLayoutConstraint(item: testLabel, attribute: .CenterY, relatedBy: .Equal, toItem: self.view, attribute: .CenterY, multiplier: 1.0, constant: 0.0)])
    }

    @IBAction func testButton(sender: AnyObject) {
        testLabel.text = "world"
    }
}

Я написал контроллер представления для теста. Этот контроллер представления представлен другим контроллером представления. Затем свойство name задается в prepareForSegue контроллера представления представления.

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    let vc = segue.destinationViewController as! TestLazyViewController
    println("vc = \(vc)")
    vc.name = "hello"
}

После запуска теста я смог получить следующий результат.

vc = <testLazy.TestLazyViewController: 0x7fb3d1d16ec0>
2015-05-25 00:26:15.673 testLazy[95577:22267122] name self = <testLazy.TestLazyViewController: 0x7fb3d1d16ec0>
2015-05-25 00:26:15.673 testLazy[95577:22267122] testLabel self = <testLazy.TestLazyViewController: 0x7fb3d1d16ec0>
2015-05-25 00:26:15.674 testLazy[95577:22267122] testLabel self = <testLazy.TestLazyViewController: 0x7fb3d1d16ec0>

Как вы можете видеть, код инициализации выполняется дважды. Я не знаю, что это ошибка или неправильное использование. Есть ли кто-нибудь, кто позволяет мне знать, что не так?

У меня также есть предположение, что неверно, что testLabel добавлен в self.view в код инициализации. Я не уверен, что код неверен. Это только моя догадка.

UPDATE:
Я до сих пор не понимаю, почему ленивая инициализация выполняется дважды. Это действительно ошибка Swift?

ЗАКЛЮЧИТЕЛЬНОЕ ОБНОВЛЕНИЕ:
@matt отлично объяснил, что эта проблема инициализируется дважды. Хотя все это исходит из моего неправильного кода, я мог бы получить ценные знания о том, как работает ключевое слово lazy. Спасибо матовый.

Ответ 1

Вся концепция вашего кода неверна.

  • В prepareForSegue вы не должны ссылаться на интерфейс контроллера представления назначения, поскольку он не имеет интерфейса. viewDidLoad еще не запущен; контроллер просмотра не имеет вида, нет выходов, ничего нет.

  • Ваш ленивый инициализатор для свойства метки не должен также добавлять метку к интерфейсу. Он должен просто сделать ярлык и вернуть его.

Другие вещи, которые нужно знать:

  • Обращаясь к контроллеру представления view, прежде чем он имеет представление, он заставит это представление загружаться преждевременно. Выполнение этого неправильного действия может фактически вызвать просмотр нагрузки дважды, что может иметь ужасные последствия.

  • Единственный способ задать диспетчеру просмотра, независимо от того, загрузил ли его представление, не заставляя преждевременно загружать представление, - это isViewLoaded().

Правильная процедура для того, что вы хотите сделать:

  • В prepareForSegue присвойте строке имени свойству name и все. Он может иметь наблюдателя, но этот наблюдатель не должен ссылаться на view, если в это время мы не имеем view, потому что это приведет к тому, что view будет загружаться преждевременно.

  • В viewDidLoad, тогда и только тогда у нас есть представление, и теперь вы можете начать заполнять интерфейс. viewDidLoad должен создать ярлык, поместить его в интерфейс, затем выбрать свойство name и присвоить его метке.


ИЗМЕНИТЬ

Теперь, сказав все это... Что это касается вашего первоначального вопроса? Как то, что вы делаете неправильно, объясняет, что делает Swift, и что делает Swift неправильно?

Чтобы увидеть ответ, просто поставьте точку останова на:

lazy var testLabel: UILabel = {
    NSLog("testLabel self = \(self)") // breakpoint here
    // ...

Что вы увидите, так это то, что из-за того, как вы структурировали свой код, мы возвращаем значение testLabel дважды рекурсивно. Здесь стек вызовов немного упрощен:

prepareForSegue
name.didset
testLabel.getter -> *
viewDidLoad
testLabel.getter -> *

Геттер testLabel относится к контроллеру представления view, который вызывает загрузку представления контроллера представления, и поэтому его вызов viewDidLoad вызывается и вызывает повторный вызов геттера testLabel.

Обратите внимание, что получатель не просто вызывается дважды в последовательности. Он вызывается дважды рекурсивно: он сам по себе сам называет себя.

Именно эта рекурсия, с которой Свифт не защищается. Если сеттер просто вызывался дважды подряд, ленивый инициализатор не был бы вызван во второй раз. Но в вашем случае это рекурсивно. Так что правда, что второй раз, ленивый инициализатор никогда не запускался раньше. Он был запущен, но он так и не был завершен. Таким образом, Свифт оправдан в запуске его сейчас, что означает, что он снова запускает его.

Итак, в некотором смысле, да, вы поймали Свифта с его штанами, но то, что вам нужно было сделать, чтобы это произошло, настолько возмутительно, что его можно оправдано назвать вашей собственной ошибкой. Это может быть ошибка Swift, но если это так, это ошибка, которая никогда не должна встречаться в реальной жизни.


EDIT:

В видео WWDC 2016 на Swift и concurrency Apple явно говорит об этом. В Swift 1 и 2 и даже в Swift 3 переменные экземпляра lazy не являются атомарными, и поэтому инициализатор может запускаться дважды, если он вызван из двух контекстов одновременно - это именно то, что делает ваш код.