Нормально ли, что свойство 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. Спасибо, Мэтт.
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