Kotlin - Inizializzazione della proprietà usando "by lazy" vs. "lateinit"


280

In Kotlin se non vuoi inizializzare una proprietà di classe all'interno del costruttore o nella parte superiore del corpo della classe, hai sostanzialmente queste due opzioni (dal riferimento al linguaggio):

  1. Inizializzazione pigra

lazy () è una funzione che accetta un lambda e restituisce un'istanza di Lazy che può fungere da delegato per l'implementazione di una proprietà lazy: la prima chiamata a get () esegue il lambda passato a lazy () e ricorda il risultato, chiamate successive per ottenere () restituisce semplicemente il risultato memorizzato.

Esempio

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

Quindi la prima chiamata e le chiamate secondarie, ovunque si trovino, a myLazyString restituiranno "Hello"

  1. Inizializzazione tardiva

Normalmente, le proprietà dichiarate come di tipo non nullo devono essere inizializzate nel costruttore. Tuttavia, abbastanza spesso questo non è conveniente. Ad esempio, le proprietà possono essere inizializzate tramite l'iniezione delle dipendenze o nel metodo di impostazione di un test unitario. In questo caso, non è possibile fornire un inizializzatore non nullo nel costruttore, ma si desidera comunque evitare controlli null quando si fa riferimento alla proprietà all'interno del corpo di una classe.

Per gestire questo caso, è possibile contrassegnare la proprietà con il modificatore lateinit:

public class MyTest {
   
   lateinit var subject: TestSubject

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

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

Il modificatore può essere utilizzato solo sulle proprietà var dichiarate all'interno del corpo di una classe (non nel costruttore principale) e solo quando la proprietà non ha un getter o setter personalizzato. Il tipo di proprietà deve essere non nullo e non deve essere di tipo primitivo.

Quindi, come scegliere correttamente tra queste due opzioni, dal momento che entrambi possono risolvere lo stesso problema?

Risposte:


336

Ecco le differenze significative tra lateinit vare by lazy { ... }proprietà delegata:

  • lazy { ... }delegate può essere utilizzato solo per le valproprietà, mentre lateinitpuò essere applicato solo a vars, poiché non può essere compilato in un finalcampo, pertanto non è possibile garantire l'immutabilità;

  • lateinit varha un campo di supporto che memorizza il valore e by lazy { ... }crea un oggetto delegato in cui il valore viene archiviato una volta calcolato, memorizza il riferimento all'istanza delegata nell'oggetto classe e genera il getter per la proprietà che funziona con l'istanza delegata. Quindi, se hai bisogno del campo di supporto presente nella classe, usa lateinit;

  • Oltre a vals, lateinitnon può essere utilizzato per proprietà non annullabili e tipi primitivi Java (ciò è dovuto nullal valore non inizializzato);

  • lateinit varpuò essere inizializzato da qualsiasi posizione dell'oggetto, ad es. dall'interno di un codice framework, e sono possibili più scenari di inizializzazione per diversi oggetti di una singola classe. by lazy { ... }, a sua volta, definisce l'unico inizializzatore per la proprietà, che può essere modificato solo sovrascrivendo la proprietà in una sottoclasse. Se vuoi che la tua proprietà venga inizializzata dall'esterno in un modo probabilmente sconosciuto prima, usa lateinit.

  • L'inizializzazione by lazy { ... }è thread-safe per impostazione predefinita e garantisce che l'inizializzatore venga richiamato al massimo una volta (ma questo può essere modificato utilizzando un altro lazysovraccarico ). Nel caso di lateinit var, spetta al codice dell'utente inizializzare correttamente la proprietà in ambienti multi-thread.

  • Un Lazyesempio può essere salvato, passati in giro e anche utilizzato per più proprietà. Al contrario, lateinit vars non memorizza alcun ulteriore stato di runtime (solo nullnel campo per valore non inizializzato).

  • Se si detiene un riferimento a un'istanza di Lazy, isInitialized()consente di verificare se è già stato inizializzato (e è possibile ottenere tale istanza con la riflessione da una proprietà delegata). Per verificare se una proprietà lateinit è stata inizializzata, è possibile utilizzare property::isInitializedda Kotlin 1.2 .

  • Un lambda passato a by lazy { ... }può catturare riferimenti dal contesto in cui viene utilizzato nella sua chiusura . Quindi memorizzerà i riferimenti e li rilascerà solo dopo l'inizializzazione della proprietà. Ciò può portare a gerarchie di oggetti, come le attività Android, che non vengono rilasciate per troppo tempo (o mai, se la proprietà rimane accessibile e non è mai accessibile), quindi è necessario fare attenzione a ciò che si utilizza all'interno dell'inizializzatore lambda.

Inoltre, c'è un altro modo non menzionato nella domanda:, Delegates.notNull()che è adatto per l'inizializzazione differita di proprietà non nulle, comprese quelle dei tipi primitivi Java.


9
Bella risposta! Vorrei aggiungere che lateinitespone il suo campo di supporto con la visibilità del setter, quindi i modi in cui si accede alla proprietà da Kotlin e da Java sono diversi. E dal codice Java questa proprietà può essere impostata anche su nullsenza alcun controllo in Kotlin. Pertanto lateinitnon è per l'inizializzazione lenta ma per l'inizializzazione non necessariamente dal codice di Kotlin.
Michael,

Esiste qualcosa di equivalente al "!" Di Swift ?? In altre parole è qualcosa che è inizializzato in ritardo ma PUO 'essere verificato per null senza che fallisca. Il 'lateinit' di Kotlin fallisce con "la proprietà lateinit currentUser non è stata inizializzata" se si seleziona "theObject == null". Questo è super utile quando si dispone di un oggetto che non è nullo nel suo scenario di utilizzo principale (e quindi si desidera codificare contro un'astrazione in cui è non nullo), ma è nullo in scenari eccezionali / limitati (cioè: accesso a quello attualmente registrato nell'utente, che non è mai nullo tranne al momento del login iniziale / nella schermata di accesso)
March

@Marchy, puoi usare esplicitamente archiviato Lazy+ .isInitialized()per farlo. Immagino non ci sia un modo semplice per controllare una proprietà del genere a nullcausa della garanzia che non puoi ottenere nullda essa. :) Guarda questa demo .
tasto di scelta rapida,

@hotkey C'è qualche punto sull'uso di troppi by lazypuò rallentare il tempo di costruzione o il runtime?
Dr.jacky,

Mi è piaciuta l'idea di usare lateinitper eludere l'uso di nullper valore non inizializzato. Diverso da quello nullnon dovrebbe mai essere usato, e con i lateinitnull può essere eliminato. È così che adoro Kotlin :)
KenIchi l'

26

In aggiunta alla hotkeybuona risposta, ecco come scelgo tra i due in pratica:

lateinit è per l'inizializzazione esterna: quando hai bisogno di cose esterne per inizializzare il tuo valore chiamando un metodo.

ad esempio chiamando:

private lateinit var value: MyClass

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

Mentre lazyè quando utilizza solo dipendenze interne al tuo oggetto.


1
Penso che potremmo ancora inizializzare pigri anche se dipende da un oggetto esterno. Devo solo passare il valore a una variabile interna. E usa la variabile interna durante l'inizializzazione lazy. Ma è naturale come Lateinit però.
Elye,

Questo approccio genera UninitializedPropertyAccessException, ho ricontrollato che sto chiamando una funzione setter prima di utilizzare il valore. Esiste una regola specifica che mi manca con lateinit? Nella tua risposta sostituisci MyClass e Any con Android Context, questo è il mio caso.
Talha,

24

Risposta molto breve e concisa

lateinit: recentemente inizializza proprietà non nulle

A differenza dell'inizializzazione lenta , lateinit consente al compilatore di riconoscere che il valore della proprietà non nulla non è memorizzato nella fase di costruzione per essere compilato normalmente.

Inizializzazione pigra

by lazy può essere molto utile quando si implementano proprietà di sola lettura (val) che eseguono l'inizializzazione lazy in Kotlin.

by lazy {...} esegue il suo inizializzatore in cui viene utilizzata per la prima volta la proprietà definita, non la sua dichiarazione.


ottima risposta, in particolare "esegue il suo inizializzatore dove viene prima utilizzata la proprietà definita, non la sua dichiarazione"
user1489829

17

lateinit vs pigro

  1. lateinit

    i) Usalo con variabile mutabile [var]

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

    ii) Consentito solo con tipi di dati non annullabili

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

    iii) È una promessa di compilatore che il valore verrà inizializzato in futuro.

NOTA : se si tenta di accedere alla variabile lateinit senza inizializzarla, viene generato UnInitializedPropertyAccessException.

  1. pigro

    i) L'inizializzazione lenta è stata progettata per impedire l'inizializzazione non necessaria degli oggetti.

    ii) La tua variabile non verrà inizializzata se non la usi.

    iii) Viene inizializzato una sola volta. La prossima volta che lo si utilizza, si ottiene il valore dalla memoria cache.

    iv) È thread-safe (viene inizializzato nel thread in cui viene utilizzato per la prima volta. Altri thread utilizzano lo stesso valore archiviato nella cache).

    v) La variabile può essere solo val .

    vi) La variabile può essere solo non annullabile .


7
Penso che in variabile pigra non possa essere var.
Däñish Shärmà,

4

Oltre a tutte le ottime risposte, esiste un concetto chiamato caricamento lento:

Il caricamento lento è un modello di progettazione comunemente utilizzato nella programmazione per computer per rinviare l'inizializzazione di un oggetto fino al punto in cui è necessario.

Usandolo correttamente, puoi ridurre i tempi di caricamento della tua applicazione. E il modo in cui Kotlin è implementato è quello lazy()che carica il valore necessario nella variabile ogni volta che è necessario.

Ma lateinit viene usato quando sei sicuro che una variabile non sarà nulla o vuota e verrà inizializzata prima di usarla -eg nel onResume()metodo per Android- e quindi non vuoi dichiararla come un tipo nullable.


Sì, ho anche inizializzato in onCreateView, onResumee altri con lateinit, ma a volte si sono verificati errori lì (perché alcuni eventi sono iniziati prima). Quindi forse by lazypuò dare un risultato appropriato. Uso lateinitper variabili non nulle che possono cambiare durante il ciclo di vita.
CoolMind,

2

Tutto è corretto sopra, ma uno di questi è una semplice spiegazione LAZY ---- Ci sono casi in cui vuoi ritardare la creazione di un'istanza del tuo oggetto fino al suo primo utilizzo. Questa tecnica è nota come inizializzazione pigra o istanza pigra. Lo scopo principale dell'inizializzazione lenta è aumentare le prestazioni e ridurre il footprint di memoria. Se l'istanza di un'istanza del tuo tipo comporta un costo computazionale elevato e il programma potrebbe non utilizzarlo, ritardare o addirittura sprecare i cicli della CPU.


0

Se si utilizza il contenitore Spring e si desidera inizializzare un campo bean non nullable, lateinitè più adatto.

    @Autowired
    lateinit var myBean: MyBean

1
dovrebbe essere come@Autowired lateinit var myBean: MyBean
Cnfn

0

Se usi una variabile immutabile, è meglio inizializzare con by lazy { ... }o val. In questo caso puoi essere sicuro che verrà sempre inizializzato quando necessario e al massimo 1 volta.

Se vuoi una variabile non nulla, che può cambiarne il valore, usa lateinit var. Nello sviluppo di Android in seguito sarà possibile inizializzare in tali eventi come onCreate, onResume. Tenere presente che se si chiama la richiesta REST e si accede a questa variabile, ciò può comportare un'eccezione UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized, poiché la richiesta può essere eseguita più rapidamente di quella che la variabile potrebbe inizializzare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.