Kotlin - Eigenschaftsinitialisierung mit "by faul" vs. "lateinit"

278

Wenn Sie in Kotlin keine Klasseneigenschaft innerhalb des Konstruktors oder oben im Klassenkörper initialisieren möchten, haben Sie grundsätzlich diese beiden Optionen (aus der Sprachreferenz):

  1. Faule Initialisierung

lazy () ist eine Funktion, die ein Lambda verwendet und eine Instanz von Lazy zurückgibt, die als Delegat für die Implementierung einer Lazy-Eigenschaft dienen kann: Der erste Aufruf von get () führt das an Lazy () übergebene Lambda aus und merkt sich das Ergebnis und nachfolgende Aufrufe Um () zu erhalten, geben Sie einfach das gespeicherte Ergebnis zurück.

Beispiel

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

So den ersten Anruf und die subquential Anrufe, wo auch immer es ist, zu myLazyString zurückkehren wird „Hallo“

  1. Späte Initialisierung

Normalerweise müssen Eigenschaften, die als Nicht-Null-Typ deklariert sind, im Konstruktor initialisiert werden. Dies ist jedoch ziemlich oft nicht bequem. Beispielsweise können Eigenschaften durch Abhängigkeitsinjektion oder in der Setup-Methode eines Komponententests initialisiert werden. In diesem Fall können Sie im Konstruktor keinen Initialisierer ungleich Null angeben, möchten jedoch weiterhin Nullprüfungen vermeiden, wenn Sie auf die Eigenschaft im Hauptteil einer Klasse verweisen.

Um diesen Fall zu behandeln, können Sie die Eigenschaft mit dem Modifikator lateinit markieren:

public class MyTest {

   lateinit var subject: TestSubject

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

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

Der Modifikator kann nur für var-Eigenschaften verwendet werden, die im Hauptteil einer Klasse deklariert sind (nicht im primären Konstruktor), und nur, wenn die Eigenschaft keinen benutzerdefinierten Getter oder Setter hat. Der Typ der Eigenschaft darf nicht null sein und darf kein primitiver Typ sein.

Wie kann man also richtig zwischen diesen beiden Optionen wählen, da beide das gleiche Problem lösen können?

regmoraes
quelle

Antworten:

333

Hier sind die wesentlichen Unterschiede zwischen lateinit varund by lazy { ... }delegierten Eigenschaften:

  • lazy { ... }Der Delegat kann nur für valEigenschaften verwendet werden, während er lateinitnur auf vars angewendet werden kann , da er nicht zu einem finalFeld kompiliert werden kann und daher keine Unveränderlichkeit garantiert werden kann.

  • lateinit varhat ein Hintergrundfeld, by lazy { ... }in dem der Wert gespeichert wird, und erstellt ein Delegatenobjekt, in dem der Wert nach der Berechnung gespeichert wird, speichert den Verweis auf die Delegateninstanz im Klassenobjekt und generiert den Getter für die Eigenschaft, die mit der Delegateninstanz arbeitet. Wenn Sie also das in der Klasse vorhandene Hintergrundfeld benötigen, verwenden Sie lateinit;

  • Zusätzlich zu vals, lateinitkann nicht für nicht nullfähige Eigenschaften und primitive Java-Typen verwendet werden (dies liegt daran, dass nullsie für nicht initialisierte Werte verwendet werden).

  • lateinit varkann von jedem Ort aus initialisiert werden, von dem aus das Objekt gesehen wird, z. B. innerhalb eines Framework-Codes. Für verschiedene Objekte einer einzelnen Klasse sind mehrere Initialisierungsszenarien möglich. by lazy { ... }Definiert wiederum den einzigen Initialisierer für die Eigenschaft, der nur durch Überschreiben der Eigenschaft in einer Unterklasse geändert werden kann. Wenn Sie möchten, dass Ihre Eigenschaft von außen auf eine zuvor wahrscheinlich unbekannte Weise initialisiert wird, verwenden Sie lateinit.

  • Die Initialisierung by lazy { ... }ist standardmäßig threadsicher und garantiert, dass der Initialisierer höchstens einmal aufgerufen wird (dies kann jedoch durch Verwendung einer anderen lazyÜberladung geändert werden ). Im Fall von lateinit varliegt es am Code des Benutzers, die Eigenschaft in Multithread-Umgebungen korrekt zu initialisieren.

  • Eine LazyInstanz kann gespeichert, weitergegeben und sogar für mehrere Eigenschaften verwendet werden. Im Gegensatz lateinit vardazu speichern s keinen zusätzlichen Laufzeitstatus (nur nullim Feld für nicht initialisierten Wert).

  • Wenn Sie einen Verweis auf eine Instanz von haben Lazy, isInitialized()können Sie überprüfen, ob diese bereits initialisiert wurde (und Sie können eine solche Instanz mit Reflexion von einer delegierten Eigenschaft erhalten). Um zu überprüfen , ob eine lateinit Eigenschaft initialisiert wurde, können Sie verwenden , property::isInitializedda Kotlin 1.2 .

  • Ein an übergebenes Lambda by lazy { ... }kann Referenzen aus dem Kontext erfassen, in dem es zum Schließen verwendet wird. Anschließend werden die Referenzen gespeichert und erst freigegeben, sobald die Eigenschaft initialisiert wurde. Dies kann dazu führen, dass Objekthierarchien, wie z. B. Android-Aktivitäten, nicht zu lange freigegeben werden (oder jemals, wenn auf die Eigenschaft zugegriffen wird und nie zugegriffen wird). Sie sollten daher vorsichtig sein, was Sie im Initialisierer-Lambda verwenden.

Es gibt auch einen anderen Weg, der in der Frage nicht erwähnt wird: Delegates.notNull() eignet sich für die verzögerte Initialisierung von Nicht-Null-Eigenschaften, einschließlich derjenigen von Java-Primitivtypen.

Hotkey
quelle
9
Gute Antwort! Ich würde hinzufügen, dass lateinitdas Hintergrundfeld mit der Sichtbarkeit des Setters verfügbar gemacht wird, sodass die Art und Weise, wie auf die Eigenschaft von Kotlin und Java aus zugegriffen wird, unterschiedlich ist. Und aus Java-Code kann diese Eigenschaft auch nullohne Überprüfung in Kotlin festgelegt werden. Daher lateinitist nicht für die verzögerte Initialisierung, sondern für die Initialisierung nicht unbedingt aus Kotlin-Code.
Michael
Gibt es etwas, das Swifts "!" ?? Mit anderen Worten, es ist etwas, das spät initialisiert wurde, aber auf Null überprüft werden kann, ohne dass es fehlschlägt. Kotlins 'lateinit' schlägt mit "lateinit property currentUser wurde nicht initialisiert 'fehl, wenn Sie' theObject == null 'aktivieren. Dies ist sehr nützlich, wenn Sie ein Objekt haben, das in seinem Kernnutzungsszenario nicht null ist (und daher gegen eine Abstraktion codieren möchte, bei der es nicht null ist), in außergewöhnlichen / begrenzten Szenarien jedoch null ist (dh auf das aktuell protokollierte Objekt zugreifen) in Benutzer, der nie null ist, außer bei der ersten Anmeldung / auf dem Anmeldebildschirm)
März
@Marchy, Sie können explizit gespeichertes Lazy+ verwenden, .isInitialized()um dies zu tun. Ich denke, es gibt keine einfache Möglichkeit, eine solche Immobilie zu überprüfen, nullda garantiert wird, dass Sie nicht davon profitieren nullkönnen. :) Siehe diese Demo .
Hotkey
@hotkey Gibt es einen Grund, zu viele zu by lazyverwenden, um die Erstellungszeit oder die Laufzeit zu verlangsamen?
Dr. Jacky
Ich mochte die Idee lateinit, die Verwendung von nullfür nicht initialisierten Wert zu umgehen . Anders als das nullsollte nie verwendet werden, und mit lateinitNullen kann weg beseitigt werden. So liebe ich Kotlin :)
KenIchi
25

Zusätzlich zu seiner hotkeyguten Antwort wähle ich in der Praxis wie folgt zwischen den beiden:

lateinit ist für die externe Initialisierung gedacht: Wenn Sie externes Material benötigen, um Ihren Wert durch Aufrufen einer Methode zu initialisieren.

zB telefonisch:

private lateinit var value: MyClass

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

In diesem Fall werden lazynur objektinterne Abhängigkeiten verwendet.

Guillaume
quelle
1
Ich denke, wir könnten immer noch faul initialisieren, selbst wenn es von einem externen Objekt abhängt. Sie müssen den Wert nur an eine interne Variable übergeben. Verwenden Sie die interne Variable während der verzögerten Initialisierung. Aber es ist so natürlich wie Lateinit.
Elye
Dieser Ansatz löst eine UninitializedPropertyAccessException aus. Ich habe vor der Verwendung des Werts überprüft, ob ich eine Setter-Funktion aufrufe. Gibt es eine bestimmte Regel, die mir bei lateinit fehlt? In Ihrer Antwort ersetzen Sie MyClass und Any durch Android Context, das ist mein Fall.
Talha
24

Sehr kurze und prägnante Antwort

lateinit: In letzter Zeit werden Nicht-Null-Eigenschaften initialisiert

Im Gegensatz zur verzögerten Initialisierung erkennt lateinit den Compiler, dass der Wert der Nicht-Null-Eigenschaft nicht in der Konstruktorstufe gespeichert ist, um normal zu kompilieren.

faule Initialisierung

by lazy kann sehr nützlich sein, wenn schreibgeschützte (val) Eigenschaften implementiert werden, die eine Lazy-Initialisierung in Kotlin durchführen.

by faul {...} führt seinen Initialisierer dort aus, wo die definierte Eigenschaft zuerst verwendet wird, nicht seine Deklaration.

John Wick
quelle
Gute
17

lateinit vs faul

  1. lateinit

    i) Verwenden Sie es mit der veränderlichen Variablen [var]

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

    ii) Nur mit nicht nullbaren Datentypen zulässig

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

    iii) Es ist ein Versprechen an den Compiler, dass der Wert in Zukunft initialisiert wird.

HINWEIS : Wenn Sie versuchen, auf die Variable lateinit zuzugreifen , ohne sie zu initialisieren, wird eine UnInitializedPropertyAccessException ausgelöst.

  1. faul

    i) Die verzögerte Initialisierung wurde entwickelt, um eine unnötige Initialisierung von Objekten zu verhindern.

    ii) Ihre Variable wird nur initialisiert, wenn Sie sie verwenden.

    iii) Es wird nur einmal initialisiert. Wenn Sie es das nächste Mal verwenden, erhalten Sie den Wert aus dem Cache-Speicher.

    iv) Es ist threadsicher (Es wird in dem Thread initialisiert, in dem es zum ersten Mal verwendet wird. Andere Threads verwenden denselben Wert, der im Cache gespeichert ist).

    v) Die Variable kann nur val sein .

    vi) Die Variable kann nur nicht nullbar sein .

Geeta Gupta
quelle
7
Ich denke in Lazy Variable kann nicht var sein.
Däñish Shärmà
4

Zusätzlich zu all den tollen Antworten gibt es ein Konzept namens Lazy Loading:

Lazy Loading ist ein Entwurfsmuster, das üblicherweise in der Computerprogrammierung verwendet wird, um die Initialisierung eines Objekts bis zu dem Punkt zu verschieben, an dem es benötigt wird.

Bei ordnungsgemäßer Verwendung können Sie die Ladezeit Ihrer Anwendung verkürzen. Die Implementierung von Kotlin besteht darin, lazy()den erforderlichen Wert bei Bedarf in Ihre Variable zu laden.

Lateinit wird jedoch verwendet, wenn Sie sicher sind, dass eine Variable nicht null oder leer ist und initialisiert wird, bevor Sie sie verwenden -eg in onResume()Methode für Android-, und Sie sie daher nicht als nullbaren Typ deklarieren möchten.

Mehrbod Khiabani
quelle
Ja, ich initialisiert in auch onCreateView, onResumeund andere mit lateinit, aber manchmal Fehler aufgetreten gibt (weil einige Ereignisse früher begonnen). by lazyKann also vielleicht ein angemessenes Ergebnis liefern. Ich verwende lateinitfür Nicht-Null-Variablen, die sich während des Lebenszyklus ändern können.
CoolMind
2

Oben ist alles richtig, aber eine der Tatsachen einfache Erklärung LAZY ---- Es gibt Fälle, in denen Sie die Erstellung einer Instanz Ihres Objekts bis zu ihrer ersten Verwendung verzögern möchten. Diese Technik ist als verzögerte Initialisierung oder verzögerte Instanziierung bekannt. Der Hauptzweck der verzögerten Initialisierung besteht darin, die Leistung zu steigern und den Speicherbedarf zu verringern. Wenn das Instanziieren einer Instanz Ihres Typs einen hohen Rechenaufwand verursacht und das Programm sie möglicherweise nicht tatsächlich verwendet, sollten Sie die Verschwendung von CPU-Zyklen verzögern oder sogar vermeiden.

user9830926
quelle
0

Wenn Sie einen Spring-Container verwenden und ein nicht nullbares Bean-Feld initialisieren möchten, lateinitist dies besser geeignet.

    @Autowired
    lateinit var myBean: MyBean
mpprdev
quelle
1
sollte sein wie@Autowired lateinit var myBean: MyBean
Cnfn
0

Wenn Sie eine unveränderliche Variable verwenden, ist es besser, mit by lazy { ... }oder zu initialisieren val. In diesem Fall können Sie sicher sein, dass es bei Bedarf und höchstens einmal initialisiert wird.

Wenn Sie eine Nicht-Null-Variable möchten, deren Wert sich ändern kann, verwenden Sie lateinit var. In Android Entwicklung können Sie später initialisieren es in solchen Ereignissen wie onCreate, onResume. Beachten Sie, dass ein Aufruf der REST-Anforderung und der Zugriff auf diese Variable zu einer Ausnahme führen kann UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized, da die Anforderung schneller ausgeführt werden kann, als diese Variable initialisieren könnte.

CoolMind
quelle