Kotlin - Inicialización de la propiedad utilizando "por perezoso" frente a "lateinit"

280

En Kotlin, si no desea inicializar una propiedad de clase dentro del constructor o en la parte superior del cuerpo de la clase, tiene básicamente estas dos opciones (de la referencia del lenguaje):

  1. Inicialización perezosa

lazy () es una función que toma un lambda y devuelve una instancia de Lazy que puede servir como delegado para implementar una propiedad lazy: la primera llamada a get () ejecuta el lambda pasado a lazy () y recuerda el resultado, llamadas posteriores para obtener () simplemente devuelve el resultado recordado.

Ejemplo

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

Entonces, la primera llamada y las llamadas secundarias, donde sea que esté, a myLazyString devolverán "Hola"

  1. Inicialización tardía

Normalmente, las propiedades declaradas como de tipo no nulo deben inicializarse en el constructor. Sin embargo, con bastante frecuencia esto no es conveniente. Por ejemplo, las propiedades se pueden inicializar mediante la inyección de dependencia o en el método de configuración de una prueba unitaria. En este caso, no puede proporcionar un inicializador no nulo en el constructor, pero aún así desea evitar verificaciones nulas al hacer referencia a la propiedad dentro del cuerpo de una clase.

Para manejar este caso, puede marcar la propiedad con el modificador lateinit:

public class MyTest {
   
   lateinit var subject: TestSubject

   @SetUp fun setup() { subject = TestSubject() }

   @Test fun test() { subject.method() }
}

El modificador solo se puede usar en propiedades var declaradas dentro del cuerpo de una clase (no en el constructor primario), y solo cuando la propiedad no tiene un captador o definidor personalizado. El tipo de la propiedad debe ser no nulo y no debe ser un tipo primitivo.

Entonces, ¿cómo elegir correctamente entre estas dos opciones, ya que ambas pueden resolver el mismo problema?

regmoraes
fuente

Respuestas:

336

Estas son las diferencias significativas entre lateinit vary la by lazy { ... }propiedad delegada:

  • lazy { ... }delegate solo se puede usar para valpropiedades, mientras lateinitque solo se puede aplicar a vars, porque no se puede compilar en un finalcampo, por lo que no se puede garantizar la inmutabilidad;

  • lateinit vartiene un campo de respaldo que almacena el valor y by lazy { ... }crea un objeto delegado en el que el valor se almacena una vez calculado, almacena la referencia a la instancia de delegado en el objeto de clase y genera el captador de la propiedad que funciona con la instancia de delegado. Entonces, si necesita el campo de respaldo presente en la clase, use lateinit;

  • Además de vals, lateinitno se puede usar para propiedades no anulables y tipos primitivos de Java (esto se debe a que se nullusa para el valor no inicializado);

  • lateinit varse puede inicializar desde cualquier lugar desde el que se vea el objeto, por ejemplo, desde el interior de un código marco, y son posibles múltiples escenarios de inicialización para diferentes objetos de una sola clase. by lazy { ... }, a su vez, define el único inicializador de la propiedad, que solo puede modificarse al anular la propiedad en una subclase. Si desea que su propiedad se inicialice desde el exterior de una manera probablemente desconocida de antemano, úsela lateinit.

  • La inicialización by lazy { ... }es segura para subprocesos de forma predeterminada y garantiza que el inicializador se invoque como máximo una vez (pero esto puede modificarse mediante el uso de otra lazysobrecarga ). En el caso de lateinit var, depende del código del usuario inicializar la propiedad correctamente en entornos de subprocesos múltiples.

  • Una Lazyinstancia se puede guardar, pasar e incluso usarse para múltiples propiedades. Por el contrario, los lateinit vars no almacenan ningún estado de tiempo de ejecución adicional (solo nullen el campo para el valor no inicializado).

  • Si tiene una referencia a una instancia de Lazy, le isInitialized()permite verificar si ya se ha inicializado (y puede obtener dicha instancia con reflejo de una propiedad delegada). Para verificar si se ha inicializado una propiedad lateinit, puede usarla property::isInitializeddesde Kotlin 1.2 .

  • Una lambda aprobada by lazy { ... }puede capturar referencias del contexto en el que se utiliza para su cierre . Luego almacenará las referencias y las liberará solo una vez que la propiedad se haya inicializado. Esto puede conducir a que las jerarquías de objetos, como las actividades de Android, no se publiquen durante demasiado tiempo (o nunca, si la propiedad permanece accesible y nunca se accede a ella), por lo que debe tener cuidado con lo que usa dentro del lambda inicializador.

Además, hay otra forma no mencionada en la pregunta: Delegates.notNull()que es adecuada para la inicialización diferida de propiedades no nulas, incluidas las de los tipos primitivos de Java.

tecla de acceso directo
fuente
9
¡Gran respuesta! Agregaría que lateinitexpone su campo de respaldo con visibilidad del configurador, por lo que las formas en que se accede a la propiedad desde Kotlin y desde Java son diferentes. Y desde el código Java, esta propiedad se puede configurar incluso nullsin ninguna comprobación en Kotlin. Por lateinitlo tanto, no es para la inicialización diferida, sino para la inicialización no necesariamente del código de Kotlin.
Michael
¿Hay algo equivalente al "!" De Swift ?? En otras palabras, es algo que se inicializa tarde pero PUEDE comprobarse como nulo sin que falle. 'Lateinit' de Kotlin falla con "la propiedad lateinit currentUser no se ha inicializado" si marca 'theObject == null'. Esto es súper útil cuando tiene un objeto que no es nulo en su escenario de uso principal (y, por lo tanto, desea codificar contra una abstracción donde no es nulo), pero es nulo en escenarios excepcionales / limitados (es decir, acceder al registro actualmente registrado en usuario, que nunca es nulo, excepto en el inicio de sesión inicial / en la pantalla de inicio de sesión)
Marzo
@Marchy, puede usar Lazy+ almacenado explícitamente .isInitialized()para hacer eso. Supongo que no hay una forma sencilla de verificar dicha propiedad nulldebido a la garantía de que no puede obtenerla null. :) Ver esta demostración .
tecla
@hotkey ¿Hay algún punto sobre el uso de demasiados by lazypuede ralentizar el tiempo de construcción o el tiempo de ejecución?
Dr.jacky
Me gustó la idea de usar lateinitpara eludir el uso de nullun valor no inicializado. Aparte de eso null, nunca se debe usar, y con lateinitnulos se puede eliminar. Así es como amo a Kotlin :)
KenIchi
26

Además de hotkeysu buena respuesta, así es como elijo entre los dos en la práctica:

lateinit es para la inicialización externa: cuando necesita cosas externas para inicializar su valor llamando a un método.

por ejemplo llamando a:

private lateinit var value: MyClass

fun init(externalProperties: Any) {
   value = somethingThatDependsOn(externalProperties)
}

Mientras lazyes cuando solo usa dependencias internas de su objeto.

Guillaume
fuente
1
Creo que aún podríamos inicializar de manera diferida incluso si depende de un objeto externo. Solo necesita pasar el valor a una variable interna. Y use la variable interna durante la inicialización diferida. Pero es tan natural como Lateinit.
Elye
Este enfoque arroja UninitializedPropertyAccessException, verifiqué dos veces que estoy llamando a una función setter antes de usar el valor. ¿Hay alguna regla específica que me falte con lateinit? En su respuesta, reemplace MyClass y Any con Android Context, ese es mi caso.
Talha
24

Respuesta muy breve y concisa

lateinit: Inicializa propiedades no nulas últimamente

A diferencia de la inicialización diferida, lateinit permite que el compilador reconozca que el valor de la propiedad no nula no se almacena en la etapa del constructor para compilar normalmente.

Inicialización perezosa

by lazy puede ser muy útil al implementar propiedades de solo lectura (val) que realizan la inicialización diferida en Kotlin.

by lazy {...} realiza su inicializador donde la propiedad definida se usa por primera vez, no su declaración.

John Wick
fuente
gran respuesta, especialmente el "realiza su inicializador donde la propiedad definida se usa por primera vez, no su declaración"
user1489829
17

lateinit vs perezoso

  1. lateinit

    i) Úselo con la variable mutable [var]

    lateinit var name: String       //Allowed
    lateinit val name: String       //Not Allowed

    ii) Permitido solo con tipos de datos no anulables

    lateinit var name: String       //Allowed
    lateinit var name: String?      //Not Allowed

    iii) Es una promesa compilar que el valor se inicializará en el futuro.

NOTA : Si intenta acceder a la variable lateinit sin inicializarla, arroja UnInitializedPropertyAccessException.

  1. perezoso

    i) La inicialización diferida se diseñó para evitar la inicialización innecesaria de objetos.

    ii) Su variable no se inicializará a menos que la use.

    iii) Se inicializa solo una vez. La próxima vez que lo use, obtendrá el valor de la memoria caché.

    iv) Es seguro para subprocesos (se inicializa en el subproceso donde se usa por primera vez. Otros subprocesos usan el mismo valor almacenado en la memoria caché).

    v) La variable solo puede ser val .

    vi) La variable solo puede ser no anulable .

Geeta Gupta
fuente
77
Creo que en la variable perezosa no puede ser var.
Däñish Shärmà
4

Además de todas las excelentes respuestas, hay un concepto llamado carga diferida:

La carga diferida es un patrón de diseño comúnmente utilizado en la programación de computadoras para diferir la inicialización de un objeto hasta el punto en que se necesita.

Utilizándolo correctamente, puede reducir el tiempo de carga de su aplicación. Y la forma de implementación de Kotlin es mediante la lazy()cual carga el valor necesario a su variable cuando sea necesario.

Pero lateinit se usa cuando está seguro de que una variable no será nula o vacía y se inicializará antes de usarla -eg en el onResume()método para Android- y, por lo tanto, no desea declararla como un tipo anulable.

Mehrbod Khiabani
fuente
Sí, yo también inicializa en onCreateView, onResumey otra con lateinit, pero a veces no se han producido errores (debido a que algunos eventos comenzaron antes). Entonces, tal vez by lazypueda dar un resultado apropiado. Lo uso lateinitpara variables no nulas que pueden cambiar durante el ciclo de vida.
CoolMind
2

Todo es correcto anteriormente, pero uno de los hechos es una explicación simple PERDIDO ---- Hay casos en los que desea retrasar la creación de una instancia de su objeto hasta su primer uso. Esta técnica se conoce como inicialización diferida o instanciación diferida. El objetivo principal de la inicialización diferida es aumentar el rendimiento y reducir la huella de su memoria. Si crear instancias de una instancia de su tipo conlleva un gran costo computacional y el programa podría terminar sin usarlo, querrá retrasar o incluso evitar el desperdicio de los ciclos de la CPU.

usuario9830926
fuente
0

Si está utilizando Spring container y desea inicializar un campo de bean no anulable, lateinites más adecuado.

    @Autowired
    lateinit var myBean: MyBean
mpprdev
fuente
1
debería ser como@Autowired lateinit var myBean: MyBean
Cnfn
0

Si usa una variable inalterable, entonces es mejor inicializar con by lazy { ... }o val. En este caso, puede estar seguro de que siempre se inicializará cuando sea necesario y, como máximo, 1 vez.

Si desea una variable no nula, que puede cambiar su valor, use lateinit var. En el desarrollo de Android más tarde puede inicializarlo en tales eventos como onCreate, onResume. Tenga en cuenta que si llama a la solicitud REST y accede a esta variable, puede generar una excepción UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized, ya que la solicitud puede ejecutarse más rápido de lo que esa variable podría inicializar.

CoolMind
fuente