Нормально ли, что свойство 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 в коде инициализации. Я не уверен, что этот код неверен. Это только мое предположение.



Обновление:

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



ПОСЛЕДНЕЕ ОБНОВЛЕНИЕ:

@matt сделал отличное объяснение для этой проблемы, инициализируемой дважды. Хотя все вещи происходят из моего неправильного кода, я мог бы получить ценные знания о том, как работает ключевое слово lazy. Спасибо, Мэтт.

503   1  

1 ответ:

Всю концепцию вашего кода-это неправильно.

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

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

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

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

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

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

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

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


EDIT:

Теперь, сказав Все это... Какое это имеет отношение к вашему первоначальному вопросу? Как то, что вы делаете неправильно здесь, объясняет, что делает 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 снова.

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

Редактировать:

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

Comments

    Ничего не найдено.