¿Es normal que la propiedad var perezoso se inicialice dos veces?

He conocido un caso muy extraño de usar una propiedad con palabra key lazy . Sé que esta palabra key indica que una initialization de una propiedad se aplaza hasta que la variable realmente se usa y solo se ejecuta una vez .

Sin embargo, encontré un caso que ejecutaba una initialization dos veces.

 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" } } 

Escribí un controller de vista para una testing. Este controller de vista es presentado por otro controller de vista. Entonces, la propiedad de name se establece en prepareForSegue del controller de vista de presentación.

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

Después de ejecutar la testing, pude get el siguiente resultado.

 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> 

Como puede ver, un código de initialization se ejecuta dos veces. No sé si es un error o un mal uso. ¿Hay alguien que me haga saber qué está mal?

También tengo una conjetura de que no es correcto lo que testLabel se agrega a self.view en el código de initialization. No estoy seguro de que el código sea incorrecto. Es solo mi suposition.

ACTUALIZAR:
Todavía no entiendo por qué la initialization perezosa se ejecuta dos veces. ¿Es realmente el error de Swift?

ACTUALIZACIÓN FINAL:
@matt ha hecho una excelente explicación para que este problema se inicialice dos veces. Aunque todas las cosas provienen de mi código incorrecto, podría get un valioso conocimiento de cómo funciona la palabra key lazy . Gracias mate

Toda la concepción de su código es incorrecta.

  • En prepareForSegue , no debe consultar la interfaz del controller de vista de destino, ya que no tiene interfaz . viewDidLoad aún no se ha ejecutado; el controller de vista no tiene vista, ni salidas, ni nada.

  • Su inicializador perezoso para la propiedad de la label no debería agregar la label a la interfaz. Solo debe hacer la label y devolverla.

Otras cosas para saber:

  • Hacer reference a la vista del controller de view antes de que tenga una vista obligará a que la vista se cargue prematuramente. Hacer esto mal puede hacer que la vista se cargue dos veces , lo que puede tener terribles consecuencias.

  • La única forma de preguntar a un controller de vista si su vista se ha cargado todavía, sin forzar que la vista se cargue prematuramente, es con isViewLoaded() .

El procedimiento correcto para lo que quiere hacer es:

  • En prepareForSegue , asigne la cadena de nombre a una propiedad de name y eso es todo. Puede tener un observador, pero ese observador no debe hacer reference a la view si no tenemos view en ese momento, porque hacerlo hará que la view cargue prematuramente.

  • En viewDidLoad , entonces y solo entonces tenemos una vista, y ahora puede comenzar a viewDidLoad la interfaz. viewDidLoad debería crear la label, ponerla en la interfaz, luego recoger la propiedad de name y asignarla a la label.


EDITAR :

Ahora, habiendo dicho todo eso … ¿Qué tiene que ver con su pregunta original? ¿De qué manera lo que está haciendo mal aquí explica lo que Swift está haciendo, y es lo que Swift está haciendo mal?

Para ver la respuesta, simplemente coloque un punto de interrupción en:

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

Lo que verá es que, debido a la forma en que estructuró su código, obtendremos el valor de testLabel dos veces de forma recursiva . Aquí está la stack de llamadas, un poco simplificada:

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

El getter de testLabel refiere a la vista del controller de view , lo que provoca que se cargue la vista del controller de vista y, por lo viewDidLoad se llama a su viewDidLoad y hace que se testLabel a llamar a testLabel getter.

Tenga en count que el getter no se llama simplemente dos veces en secuencia. Se lo llama de manera recursiva dos veces: en sí mismo se está llamando a sí mismo.

Es esta recursión en la que Swift no puede defenderse. Si el progtwigdor se llama simplemente dos veces seguidas, el inicializador perezoso no habría sido llamado por segunda vez. Pero en tu caso, es recursivo. Entonces, es cierto que la segunda vez, el inicializador perezoso nunca se ha ejecutado antes . Se ha iniciado , pero nunca se ha completado . Por lo tanto, Swift está justificado en ejecutarlo ahora, lo que significa que se ejecutará de nuevo.

Entonces, en cierto sentido, sí, has atrapado a Swift con los pantalones bajos, pero lo que tenías que hacer para lograrlo es tan escandaloso que puede justificadamente llamarte culpa tuya. Puede ser el error de Swift, pero si es así, es un error que simplemente nunca debería encontrarse en la vida real.


EDITAR:

En el video WWDC 2016 sobre Swift y concurrency, Apple es explícito al respecto. En Swift 1 y 2, e incluso en Swift 3, las variables de instancia lazy no son atómicas y, por lo tanto, el inicializador puede ejecutarse dos veces si se llama desde dos contexts simultáneamente, que es exactamente lo que hace su código.

Intereting Posts