Swift-style-guide: Patrón de inicialización perezoso

Creado en 15 abr. 2015  ·  29Comentarios  ·  Fuente: raywenderlich/swift-style-guide

Muchos de nosotros en los tutoriales en estos días estamos usando patrones como este para inicializar variables:

lazy var locationManager: CLLocationManager = {
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
  }()

He visto esto para configurar controles como UILabels y también. Esto tiene la ventaja de mantener el código de inicialización en un lugar práctico.

Pensé en iniciar una discusión para ver si este es un patrón que queremos fomentar en la guía de estilo.

Comentario más útil

Para su información, Joe Groff de Swift ha confirmado que los vars perezosos son automáticamente @noescape para que no se capturen a sí mismos.

https://twitter.com/jckarter/status/704100315587477504

Tenga en cuenta que aún puede capturarse a sí mismo de otras maneras (por ejemplo, bloquear variables que se capturan a sí mismo dentro de una variable perezosa)

Todos 29 comentarios

por cierto, ¿se retiene cíclicamente aquí? Creo que se debe agregar [unowned self] o [weak self]

tambien tengo una pregunta:
¿Cuál es el nombre recomendado para strongSelf en este caso? No he encontrado esto en su guía:

if let strongSelf = self {

}

No creo que importe si self se retiene o no, porque el cierre se ejecuta cada vez que se usa la variable locationManager y luego se destruye.

Aunque no estoy del todo seguro de lo que sucede cuando nunca llamas a locationManager . En ese caso, el cierre puede ser, de hecho, aferrarse a uno mismo. Interesante. :-)

Editar: Acabo de probar esto y usar self en este cierre parece estar bien. Supongo que el cierre realmente no se crea hasta que llamas a locationManager por primera vez. Y luego termina inmediatamente y se destruye nuevamente.

Por cierto, el ! no es necesario; esto funciona bien:

lazy var locationManager: CLLocationManager = {

Gracias, actualicé el bloque de código original para eliminar el ! . Pero aún tenemos que discutir la pregunta principal: ¿deberíamos fomentar este patrón en nuestra guía de estilo o no?

Me gusta. Mantiene la inicialización de la cosa cerca de la declaración de la cosa. Como resultado, viewDidLoad() tiende a ser más corto y más fácil de leer.

A mí también me gusta. 👍

Si hay problemas con un ciclo de referencia sólido, creo que [unowned self] debería solucionarlo:

lazy var locationManager: CLLocationManager = {
  [unowned self] in
  let manager = CLLocationManager()
  manager.delegate = self
  ...
  return manager
}()

Me gusta este enfoque, pero me cuesta ver las objeciones. ¿Es solo que este enfoque no es tan obvio y que puede tener matices sutiles (como el potencial de fuertes ciclos de retención)?

(referencia: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID57)

En mi opinión, esto es solo un patrón. Tal vez pertenezca a un repositorio de patrones rápidos. En cuanto a los ciclos de retención, me lo estoy perdiendo. Es solo una asignación típica de delegado. ¿Por qué debería tratarse esto de manera diferente que si solo hiciera las cosas de inicialización de CLLocationManager en un método de inicialización? El patrón de delegado generalmente alienta a que la propiedad del delegado (en este caso, dentro de CLLocationManager) se defina como débil, ¿no?

Mi error re fuerte retener ciclos. No es un problema porque se ejecuta el cierre. Si devolviera un cierre, entonces podría ser necesario usar [unowned self] , aunque no en este caso porque se le asigna como un delegado débil.

También vale la pena tener en cuenta que el cierre especificado para generar la var perezosa no debe depender de ninguna propiedad modificable de la clase. En el momento en que se genera la var perezosa (a través de la evaluación del cierre), capturará los valores actuales de estas dependencias. Será confuso si la var no cambia.

Vea este ejemplo de http://mikebuss.com/2014/06/22/lazy-initialization-swift/

class Person {
    var name: String

    lazy var personalizedGreeting: String = {
        [unowned self] in
        return "Hello, \(self.name)!"
        }()

    init(name: String) {
        self.name = name
    }
}

Creo que esto es confuso:

let p = Person(name: "sam")
p.personalizedGreeting
// => "Hello, sam!"

p.name = "definitely somebody else"
p.personalizedGreeting
// => "Hello, sam!"

Después de todas estas divagaciones, sigo pensando que es un buen patrón y adecuado para su uso en tutoriales, pero es importante que los autores comprendan estos problemas para no cometer estos errores relativamente complejos.

/ Me callaré ahora

En los documentos de Apple que @sammyd vincula, Apple en realidad da esto como ejemplo:

lazy var asHTML: () -> String = {
    [unowned self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

En el segundo enlace que da @sammyd (el enlace está feliz hoy :] ), el autor da esto como ejemplo:

lazy var personalizedGreeting: String = {
        [unowned self] in
        return "Hello, \(self.name)!"
        }()

No creo que usar unowned self alguna vez sea un problema en estos casos... es decir, en la creación de propiedades perezosas... Sin embargo, imagino que en ciertos casos (como menciona @sammyd nuevamente, devolver un bloque, ¿quizás otros?) _no_ usar unowned podría ser problemático.

Para simplificar, ¿por qué no usar siempre unowned para propiedades perezosas?

Independientemente, estoy a favor de usar este patrón. :+1:

Tenga en cuenta que la variable asHTML _almacena_ el cierre para su uso posterior. De lo que estamos hablando aquí es de inicializar una variable regular usando un cierre que se ejecuta inmediatamente.

Me gusta, tanto para las propiedades lazy como para las que no son lazy .

Sin embargo, no estoy seguro de cuál sería la orientación en esta guía. _Siempre_ hacerlo de esa manera? Si tenía algunas constantes o colores calculados, también puedo ver el beneficio de "la forma antigua" de inicializar varias cosas en viewDidLoad() para reutilizar objetos. Se siente como una cosa de caso por caso para mí.

He usado llamadas initWithFrame para instanciar vistas de ui durante años con subvistas de uilabels / textviews / y simplifica el código y evita tropezar con las llamadas del ciclo de vida de la vista, particularmente cuando se llama a superclases.
¿Sufre un pequeño golpe de rendimiento? Quizás. ¿Esto va en contra del flujo de uikit y el uso de archivos nib? Si. ¿Paso horas solucionando problemas de archivos nib? No. ¿Mis aplicaciones se envían a tiempo? Si. ¿Estoy preocupado? No. ¿Están horrorizados otros desarrolladores? probablemente. ¿Me importa? - No.
Código simple = Lógica simple - Fácil de mantener.

Algunos argumentarían que para el rendimiento o la memoria hasta que se presente el controlador de vista (se presionó en el controlador de navegación), entonces no instancia los controles (espere a viewDidLoad / loadView). Eso fue antes de que tuviéramos acceso a lazy vars en los controladores de vista.
Así que esto resuelve el problema de la instanciación preventiva.

p.ej.
lazy var loginVC = { return LoginVC()}()

Nota: he anulado el método init aquí (sin archivos xib)

required public init() {
    super.init(nibName: nil, bundle: nil)
}

De stackoverflow
http://stackoverflow.com/questions/27585409/using-a-programmatically-created-uitableviewcell-subclass-in-swift/31801507#31801507

Esta

clase EventCell: UITableViewCell {

var eventName: UILabel = UILabel()
var eventCity: UILabel = UILabel()
var eventTime: UILabel = UILabel()

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    self.contentView.addSubview(eventName)
    self.contentView.addSubview(eventCity)
    self.contentView.addSubview(eventTime)
}

override func layoutSubviews() {
    super.layoutSubviews()

    eventName = UILabel(frame: CGRectMake(20, 10, self.bounds.size.width - 40, 25))
    eventCity = UILabel(frame: CGRectMake(0, 0, 0, 0))
    eventTime = UILabel(frame: CGRectMake(0, 0, 0, 0))

}

}

contra

class EventCell: UITableViewCell {
   lazy public  var lblName = {
    return UILabel (frame: CGRectMake(10, 0, self.bounds.size.width , 40))
  }()

  lazy public  var lblCity = {
   return UILabel (frame: CGRectMake(10, 40, self.bounds.size.width , 20))
  }()
 lazy public  var lblTime = {
      return UILabel (frame: CGRectMake(self.bounds.size.width - 80, 10, , 25))
   }()


   override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
   super.init(style: style, reuseIdentifier: reuseIdentifier)

    self.contentView.addSubview(lblName)
    self.contentView.addSubview(lblCity)
    self.contentView.addSubview(lblTime)
 }

}  

Aproximadamente [unowned self] utilizados para evitar el ciclo de retención: ya no es el caso.

El código de muestra proporcionado por Apple y vinculado anteriormente por @sammyd ya no usa [unowned self] :

lazy var asHTML: Void -> String = {
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

Y, de hecho, nunca podría perder un deinit usando una var perezosa inicializada con un cierre que hace referencia a un self desnudo.

Ahora no sé si self es implícitamente unowned o unowned(unsafe) en esos cierres. Sin embargo, deberíamos dejar de hacerlo explícito, ya que es implícito.

El ejemplo de asHTML no está inicializando un valor con un cierre, está almacenando un cierre en una variable. En el caso de asHTML, todavía se requiere [yo sin propietario] para evitar un ciclo de referencia fuerte. Todo esto se menciona en la documentación de Swift vinculada.

Eric Chamberlain, arquitecto principal - iOS
ArcTouch - Estudio de desarrollo de aplicaciones

eric [email protected] mailto: [email protected]
+1-415-944-2000

Aplicaciones personalizadas para marcas de clase mundial y Fortune 500
arctouch.com/trabajo | arctouch.com/blog

El 8 de diciembre de 2015, a las 8:18 a. m., Gwendal Roué [email protected] escribió:

Acerca de [yo sin propietario] utilizado para evitar el ciclo de retención: ya no es el caso.

El código de muestra https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097 -CH20-ID57 proporcionado por Apple y vinculado anteriormente por @sammyd https://github.com/sammyd ya no usa [yo sin dueño]:

lazy var asHTML: Vacío -> Cadena = {
if dejar texto = self.text {
devuelve "<(self.name)>(text)(self.name)>"
} demás {
devuelve "<(self.name) />"
}
}
Y, de hecho, nunca podría perder un deinit usando una var perezosa inicializada con un cierre que hace referencia a un yo desnudo.

Ahora no sé si el yo está implícitamente sin dueño o sin dueño (inseguro) en esos cierres. Sin embargo, deberíamos dejar de hacerlo explícito, ya que es implícito.


Responda a este correo electrónico directamente o véalo en GitHub https://github.com/raywenderlich/swift-style-guide/issues/88#issuecomment -162933300.

@arctouch-ericchamberlain, tiene razón, el código de muestra ahora almacena un cierre en la propiedad.

Sin embargo, me repito, "nunca podría perderme un deinit usando una var perezosa inicializada con un cierre que hace referencia a un yo desnudo".

Por ejemplo:

class C {
    lazy var d: String = { "\(self)" }()
    deinit { print("deinit") }
}

C()   // deinit
C().d // deinit

Puedes probarlo tú mismo. Si encuentra un ciclo de retención, me complacería volver a usar [unowned self] .

Eso es porque

perezoso var algo = {..}()

no retiene nada. El cierre se construye y ejecuta cuando se inicializa la variable. Mientras que,

perezoso var algo = {…}

crea un fuerte ciclo de referencia cuando se inicializa la variable.

La variable asHTML en el código de ejemplo cae en el último caso.

Elimine () de su inicializador, luego vea qué sucede.

Eric Chamberlain, arquitecto principal - iOS
ArcTouch - Estudio de desarrollo de aplicaciones

eric [email protected] mailto: [email protected]
+1-415-944-2000

Aplicaciones personalizadas para marcas de clase mundial y Fortune 500
arctouch.com/trabajo | arctouch.com/blog

El 9 de diciembre de 2015, a las 2:10 p. m., Gwendal Roué [email protected] escribió:

@arctouch-ericchamberlain https://github.com/arctouch-ericchamberlain , tiene razón, el código de muestra ahora almacena un cierre en la propiedad.

Sin embargo, me repito, "nunca podría perderme un deinit usando una var perezosa inicializada con un cierre que hace referencia a un yo desnudo".

Por ejemplo:

clase C {
lazy var d: String = { "(self)" }()
deinit { print("deinit") }
}

C() // definir
C().d // definir
Puedes probarlo tú mismo. Si encuentra un ciclo de retención, me complacería volver a usar [yo sin propietario].


Responda a este correo electrónico directamente o véalo en GitHub https://github.com/raywenderlich/swift-style-guide/issues/88#issuecomment -163413290.

no te sigo

Ninguno de esos vars perezosos requiere [unowned self] :

lazy var d: String = { "\(self)" }()
lazy var asHTML: Void -> String = { "\(self)" }

¿Qué tipo de var perezoso hace?

Ups, llamar a asHTML de hecho crea el ciclo de retención.

Gracias @arctouch-ericchamberlain. me he equivocado

@grupo

La razón por la que descubrió que los documentos de Apple no usan [unowned self] es porque están configurando la situación en la que hay un problema:

Desafortunadamente, la clase HTMLElement, como se escribió anteriormente, crea un fuerte ciclo de referencia entre una instancia de HTMLElement y el cierre utilizado para su valor predeterminado asHTML.

Luego continúan solucionando esto con la adición de [unowned self] :

Una referencia sin propietario es el método de captura apropiado para resolver el ciclo de referencia fuerte en el ejemplo de HTMLElement anterior.

Lo que resulta en esta implementación:

lazy var asHTML: Void -> String = {
    [unowned self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

Esa página de documentación tiene una buena descripción de cuándo son posibles los ciclos de retención y cuándo debe usar unowned o weak para salir de ellos.

@johndpope

Trate de comparar con esta implementación, no estoy seguro de lo que me perdí, pero las muestras que he visto de Apple tienden a evitar los corchetes de cierre solo para configurar cosas.

class EventCell: UITableViewCell {

    lazy public var lblName = UILabel(frame: CGRectZero)
    lazy public var lblCity = UILabel(frame: CGRect())
    lazy public var lblTime = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))

} 

No estoy de acuerdo con la noción de que lazy debe usarse para mejorar la legibilidad. Su único propósito es realmente el rendimiento, ya que podría presentar problemas con el estado, por ejemplo, el uso de propiedades mutables dentro del cierre. Ya tiene la inicialización en un lugar, se llama init designado.

Está utilizando perezoso porque tiene algo pesado que podría no ejecutarse, por lo tanto, no es necesario o desea retrasar su ejecución, lo que coincidentemente permite que el usuario cancele.

Entonces, lo más probable es que el cierre se conserve solo después de que usted sea propietario de dicha clase, creando una fuga.
Siempre use débil/sin propietario, incluso si está 100% seguro de que se llamará al cierre.
Eventualmente, sospecho que el compilador manejará eso. Una propiedad perezosa con un cierre solo se puede ejecutar una vez, piense dispatch_once(..) . De todos modos, probablemente así es como voy a rodar.

Primero, dejemos de lado la referencia a la memoria. (Incluso las publicaciones de blog más exitosas sobre perezosas se equivocan. ¡Tenemos que cambiar eso, Maxime!) Por las razones descritas anteriormente, esto no se filtra. Es decir: el cierre se ejecuta perezosamente una vez. Si se ejecuta, self se retiene y luego se libera. Si no se ejecuta, nunca se conserva.

El siguiente código de muestra definitivamente muestra esto:

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

class Person {
  init(name: String) {
    self.name = name
    print("\(name) is born.")
  }

  deinit {
    print("\(name) dies.")
  }

  var name: String

  lazy var greeting: String = {
    return "Hello \(self.name)!"
  }()

}

do {
  let t = Person(name: "Ray")
  print(t.greeting)
}

print ("Next")

// Prints: 
// Ray is born.
// Hello Ray!
// Ray dies.
// Next

do {
  let t = Person(name: "Ray")
}

print ("End of the world")

// Prints:
// Ray is born.
// Ray dies.
// End of the world

A continuación, veamos todo este asunto de la pereza. Esta es una excelente manera de controlar la inicialización con respecto al controlador de vista, ya que paga exactamente en el punto de uso. Es particularmente importante para CLLocationManager , ya que es probable que desee controlar cuándo ocurre el acceso para que no solicite permisos a mitad de camino.

Otro patrón que tiene un aspecto un poco menos javascript es hacer un método privado llamado y llamarlo:

lazy var locationManager: CLLocationManager = self.makeLocationManager()

private func makeLocationManager() -> CLLocationManager
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
}

Un par de notas:

  • No estoy seguro de qué versión de Swift se hizo posible.
  • Sin fugas de montón; mismas razones anteriores. ¿No me crees? ¡Intentalo!
  • locationManager tiene una anotación de tipo explícita. El compilador lo requiere actualmente o recibe un mensaje de error críptico sobre self sin resolver.
  • Funciona para clases pero no para estructuras inmutables.

Para los componentes de la interfaz de usuario que no están asignados en un guión gráfico o incondicionalmente en viewDidLoad() , creo que lazy es un buen patrón. Pero no creo que esté listo para decir "todas las cosas son flojas", ya que puede requerir algunos trucos como LazyBoxes para sortear los errores de inmutabilidad.

Debo mencionar que, aunque no ha sido aceptado, probablemente haya un cambio bastante grande en la forma en que funciona la pereza:
https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md

Qué piensas sobre esto:

Perezoso
Considere usar la inicialización diferida para un control de grano más fino sobre la vida útil del objeto.

// Mark: - Location services
extension MyViewController {
  lazy var locationManager: CLLocationManager = self.makeLocationManager()

  private func makeLocationManager() -> CLLocationManager
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
  }
  // :
}

No me importa la inicialización de estilo de bloque de variables. De hecho, al colocar dichas inicializaciones en la declaración de variables, se permite una anulación sencilla de las pruebas de unidad/interfaz de usuario.

Por cierto, definir locationManager como propiedad de clase no funciona debido a la asignación de manager.delegate = self

¿Que sugieres?

Simplemente haga que su clase (¿controlador de vista?) Se ajuste a CLLocationManagerDelegate.

@rayfix He movido este fragmento a superclase. No incluí el protocolo CLLocationManagerDelegate en la base.

Gracias 😀

Para su información, Joe Groff de Swift ha confirmado que los vars perezosos son automáticamente @noescape para que no se capturen a sí mismos.

https://twitter.com/jckarter/status/704100315587477504

Tenga en cuenta que aún puede capturarse a sí mismo de otras maneras (por ejemplo, bloquear variables que se capturan a sí mismo dentro de una variable perezosa)

Solo quería hacerle saber que al usar la nueva vista de gráfico de memoria en Xcode 8, descubrí que había una captura cuando no usé la autoconstrucción sin dueño en lo siguiente:

lazy var locationManager: CLLocationManager = {
    [unowned self] in
    var _locationManager = CLLocationManager()
    _locationManager.delegate = self
    _locationManager.desiredAccuracy = Set.shared.trackingAccuracy
    _locationManager.allowsBackgroundLocationUpdates = true
    _locationManager.activityType = .fitness      
    _locationManager.distanceFilter = Double(Set.shared.waypointInterval)
    return _locationManager
}()

Pero no ocurrió cuando lo incluí y no hice otros cambios.

@rondiel tenga en cuenta que el delegado de CLLocationManager se declaró como asignado (SDK de iOS 10)
@property(assign, nonatomic, nullable) id<CLLocationManagerDelegate> delegate;

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

jrturton picture jrturton  ·  3Comentarios

sima-11 picture sima-11  ·  5Comentarios

gokselkoksal picture gokselkoksal  ·  9Comentarios

fabienwarniez picture fabienwarniez  ·  9Comentarios

hollance picture hollance  ·  28Comentarios