Kotlin - Inicjalizacja nieruchomości przy użyciu „przez leniwego” vs. „lateinit”

279

W Kotlin, jeśli nie chcesz inicjować właściwości klasy wewnątrz konstruktora lub w górnej części ciała klasy, masz w zasadzie te dwie opcje (z odwołania do języka):

  1. Leniwa inicjalizacja

lazy () to funkcja, która pobiera lambda i zwraca instancję Lazy, która może służyć jako delegat do implementacji właściwości lazy: pierwsze wywołanie get () wykonuje lambda przekazane do lazy () i zapamiętuje wynik, kolejne wywołania get () po prostu zwraca zapamiętany wynik.

Przykład

public class Hello {

   val myLazyString: String by lazy { "Hello" }

}

Tak więc pierwsze wywołanie i wywołania podrzędne, gdziekolwiek się znajdują, myLazyString zwrócą „Cześć”

  1. Późna inicjalizacja

Zwykle właściwości zadeklarowane jako niepuste muszą zostać zainicjowane w konstruktorze. Jednak dość często nie jest to wygodne. Na przykład właściwości można zainicjować przez wstrzyknięcie zależności lub w metodzie konfiguracji testu jednostkowego. W takim przypadku nie można podać w konstruktorze inicjatora o wartości innej niż NULL, ale nadal należy unikać sprawdzania wartości NULL podczas odwoływania się do właściwości wewnątrz ciała klasy.

Aby obsłużyć ten przypadek, możesz oznaczyć właściwość modyfikatorem lateinit:

public class MyTest {
   
   lateinit var subject: TestSubject

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

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

Modyfikatora można używać tylko w przypadku właściwości var zadeklarowanych w treści klasy (nie w głównym konstruktorze) i tylko wtedy, gdy właściwość nie ma niestandardowego programu pobierającego ani ustawiającego. Typ właściwości musi być różny od null i nie może być typem pierwotnym.

Jak więc właściwie wybrać jedną z tych dwóch opcji, skoro obie mogą rozwiązać ten sam problem?

regmoraes
źródło

Odpowiedzi:

334

Oto znaczące różnice między właściwością delegowaną lateinit vara by lazy { ... }:

  • lazy { ... }delegata może być używana tylko dla valwłaściwości, podczas gdy lateinitmoże być stosowana tylko do vars, ponieważ nie można go skompilować do finalpola, dlatego nie można zagwarantować niezmienności;

  • lateinit varma pole kopii zapasowej, które przechowuje wartość i by lazy { ... }tworzy obiekt delegowany, w którym wartość jest przechowywana po obliczeniu, przechowuje odwołanie do instancji delegowanej w obiekcie klasy i generuje moduł pobierający dla właściwości, która działa z instancją delegowaną. Jeśli więc potrzebujesz pola podkładu obecnego w klasie, użyj lateinit;

  • Oprócz vals lateinitnie można go używać w przypadku właściwości nie dopuszczających wartości zerowych i typów pierwotnych Java (wynika to z nullużycia niezainicjowanej wartości);

  • lateinit varmożna zainicjować z dowolnego miejsca, z którego widziany jest obiekt, np. z kodu frameworka, i możliwe są różne scenariusze inicjowania dla różnych obiektów jednej klasy. by lazy { ... }, z kolei definiuje jedyny inicjator właściwości, który można zmienić tylko poprzez przesłonięcie właściwości w podklasie. Jeśli chcesz, aby Twoja własność została zainicjowana z zewnątrz w sposób prawdopodobnie nieznany wcześniej, użyj lateinit.

  • Inicjalizacja by lazy { ... }jest domyślnie bezpieczna dla wątków i gwarantuje, że inicjator zostanie wywołany co najwyżej jeden raz (ale można to zmienić, stosując inne lazyprzeciążenie ). W przypadku lateinit varkodu użytkownika poprawna inicjalizacja właściwości w środowiskach wielowątkowych zależy od kodu użytkownika.

  • LazyInstancji mogą być zapisywane, przekazywane wokół i nawet wykorzystywane do wielu właściwości. Przeciwnie, lateinit vars nie przechowują żadnego dodatkowego stanu środowiska wykonawczego (tylko nullw polu dla niezainicjowanej wartości).

  • Jeśli masz odwołanie do wystąpienia Lazy, isInitialized()pozwala sprawdzić, czy zostało ono już zainicjowane (i możesz uzyskać takie wystąpienie za pomocą właściwości delegowanej). Aby sprawdzić, czy właściwość lateinit została zainicjowana, możesz jej używać property::isInitializedod wersji Kotlin 1.2 .

  • Przekazana lambda by lazy { ... }może przechwytywać referencje z kontekstu, w którym jest używana do zamknięcia . Następnie zapisze odniesienia i zwolni je dopiero po zainicjowaniu właściwości. Może to prowadzić do tego, że hierarchie obiektów, takie jak działania Androida, nie zostaną zwolnione zbyt długo (lub nigdy, jeśli właściwość pozostaje dostępna i nigdy nie jest dostępna), dlatego należy zachować ostrożność przy używaniu lambda inicjalizatora.

Jest też inny sposób niewymieniony w pytaniu:, Delegates.notNull()który jest odpowiedni do odroczonej inicjalizacji właściwości niepustych, w tym właściwości pierwotnych typów Java.

Klawisz skrótu
źródło
9
Świetna odpowiedź! Dodałbym, że lateinitujawnia swoje pole zaplecza z widocznością setera, więc sposoby dostępu do właściwości z Kotlin i Java są różne. Z kodu Java można ustawić tę właściwość nawet nullbez sprawdzania w Kotlin. Dlatego lateinitnie jest do leniwej inicjalizacji, ale do inicjalizacji niekoniecznie z kodu Kotlin.
Michael
Czy istnieje coś równoważnego „!” Swifta ?? Innymi słowy, jest to coś, co zostało zainicjowane z opóźnieniem, ale MOŻE być sprawdzone pod kątem wartości zerowej bez niepowodzenia. „Lateinit” Kotlina kończy się niepowodzeniem, gdy „właściwość lateinit currentUser nie została zainicjowana”, jeśli zaznaczysz „theObject == null”. Jest to bardzo przydatne, gdy masz obiekt, który nie ma wartości null w podstawowym scenariuszu użycia (i dlatego chcesz zakodować abstrakcję tam, gdzie nie jest on zerowy), ale ma wartość null w scenariuszach wyjątkowych / ograniczonych (tj .: dostęp do aktualnie zalogowanego w użytkowniku, która nigdy nie jest zerowa, z wyjątkiem pierwszego logowania / na ekranie logowania)
Marzec
@Marchy, możesz użyć do tego jawnie zapisanego Lazy+ .isInitialized(). Myślę, że nie ma prostego sposobu na sprawdzenie takiej nieruchomości ze nullwzględu na gwarancję, że nie można nullz niej uzyskać . :) Zobacz to demo .
skrót
@hotkey Czy jest sens używania zbyt wielu, by lazymoże spowolnić czas kompilacji lub środowisko uruchomieniowe?
Dr.jacky
Podobał mi się pomysł użycia w lateinitcelu obejścia użycia nullniezainicjowanej wartości. Poza tym nullnigdy nie należy go używać, a lateinitwartości zerowe można wyeliminować. Tak uwielbiam Kotlin :)
KenIchi,
26

Oprócz hotkeydobrej odpowiedzi, oto jak wybieram jedną z dwóch w praktyce:

lateinit służy do zewnętrznej inicjalizacji: gdy potrzebujesz zewnętrznych rzeczy do zainicjowania wartości przez wywołanie metody.

np. dzwoniąc:

private lateinit var value: MyClass

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

Podczas lazygdy używa tylko zależności wewnętrznych dla twojego obiektu.

Guillaume
źródło
1
Myślę, że możemy nadal leniwie inicjalizować, nawet jeśli zależy to od zewnętrznego obiektu. Wystarczy przekazać wartość do zmiennej wewnętrznej. I używaj zmiennej wewnętrznej podczas leniwej inicjalizacji. Ale to jest tak naturalne jak Lateinit.
Elye
Takie podejście powoduje wyjątek UninitializedPropertyAccessException, dwukrotnie sprawdziłem, czy wywołuję funkcję ustawiającą przed użyciem wartości. Czy jest jakaś konkretna zasada, której brakuje mi w Lateinit? W swojej odpowiedzi zastąp MyClass i Any kontekstem Androida, to moja sprawa.
Talha,
24

Bardzo krótka i zwięzła odpowiedź

lateinit: Ostatnio inicjuje właściwości niepuste

W przeciwieństwie do opóźnionego inicjowania, lateinit pozwala kompilatorowi rozpoznać, że wartość właściwości innej niż null nie jest przechowywana na etapie konstruktora w celu normalnej kompilacji.

leniwa inicjalizacja

by lazy może być bardzo przydatny przy implementacji właściwości tylko do odczytu (val), które wykonują leniwą inicjalizację w Kotlinie.

przez lazy {...} wykonuje inicjalizator, w którym najpierw używana jest zdefiniowana właściwość, a nie deklaracja.

John Wick
źródło
świetna odpowiedź, zwłaszcza „wykonuje inicjalizator tam, gdzie najpierw używana jest zdefiniowana właściwość, a nie deklaracja”
user1489829
17

Lateinit vs leniwy

  1. późno

    i) Użyj go ze zmienną zmienną [var]

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

    ii) Dozwolone tylko w przypadku typów danych, które nie mają wartości dopuszczających wartości zerowe

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

    iii) Kompilator obiecuje, że wartość zostanie zainicjowana w przyszłości.

UWAGA : Jeśli spróbujesz uzyskać dostęp do zmiennej lateinit bez jej inicjalizacji, zgłasza wyjątek UnInitializedPropertyAccessException.

  1. leniwy

    i) Leniwa inicjalizacja została zaprojektowana w celu zapobiegania niepotrzebnej inicjalizacji obiektów.

    ii) Twoja zmienna nie zostanie zainicjalizowana, dopóki jej nie użyjesz.

    iii) Jest inicjowany tylko raz. Następnym razem, gdy go użyjesz, otrzymasz wartość z pamięci podręcznej.

    iv) Jest bezpieczny dla wątków (jest inicjowany w wątku, w którym jest używany po raz pierwszy. Inne wątki używają tej samej wartości przechowywanej w pamięci podręcznej).

    v) Zmienna może być tylko val .

    vi) Zmienna może być tylko niedopuszczalna .

Geeta Gupta
źródło
7
Myślę, że zmienna leniwa nie może być var.
Däñish Shärmà,
4

Oprócz wszystkich świetnych odpowiedzi istnieje koncepcja zwana leniwym ładowaniem:

Leniwe ładowanie jest wzorcem projektowym powszechnie stosowanym w programowaniu komputerowym w celu odroczenia inicjalizacji obiektu do momentu, w którym jest on potrzebny.

Używając go poprawnie, możesz skrócić czas ładowania aplikacji. A sposób implementacji Kotlina polega na lazy()tym, że ładuje potrzebną wartość do twojej zmiennej, ilekroć jest potrzebna.

Ale Lateinit jest używany, gdy masz pewność, że zmienna nie będzie pusta ani pusta i zostanie zainicjowana przed użyciem jej -eg w onResume()metodzie dla Androida- i dlatego nie chcesz deklarować jej jako typu zerowalnego.

Mehrbod Khiabani
źródło
Tak, ja też zainicjowany w onCreateView, onResumea drugi z lateinit, ale czasami nie wystąpiły błędy (ponieważ niektóre wydarzenia zaczął wcześniej). Może by lazymoże dać odpowiedni wynik. Używam lateinitdla zmiennych niepustych, które mogą się zmieniać podczas cyklu życia.
CoolMind,
2

Powyżej wszystko jest poprawne, ale jedno z faktów proste wyjaśnienie LAZY ---- Są przypadki, kiedy chcesz opóźnić utworzenie instancji swojego obiektu do jego pierwszego użycia. Ta technika jest znana jako leniwa inicjalizacja lub leniwa instancja. Głównym celem leniwej inicjalizacji jest zwiększenie wydajności i zmniejszenie zużycia pamięci. Jeśli utworzenie instancji tego typu wiąże się z dużymi kosztami obliczeniowymi, a program może w rzeczywistości z niej nie skorzystać, warto opóźnić lub nawet uniknąć marnowania cykli procesora.

użytkownik9830926
źródło
0

Jeśli używasz kontenera Spring i chcesz zainicjować pole fasoli innej niż zerowa, lateinit ma wartości lepiej się nadaje.

    @Autowired
    lateinit var myBean: MyBean
mpprdev
źródło
1
powinno być jak@Autowired lateinit var myBean: MyBean
Cnfn
0

Jeśli używasz niezmiennej zmiennej, lepiej zainicjować za pomocą by lazy { ... }lubval . W takim przypadku możesz być pewien, że będzie on zawsze inicjowany w razie potrzeby i maksymalnie 1 raz.

Jeśli potrzebujesz zmiennej innej niż null, która może zmienić jej wartość, użyj lateinit var. W rozwoju Android można później zainicjować w takich imprezach jak onCreate, onResume. Należy pamiętać, że jeśli wywołasz żądanie REST i uzyskasz dostęp do tej zmiennej, może to prowadzić do wyjątku UninitializedPropertyAccessException: lateinit property yourVariable has not been initialized, ponieważ żądanie może zostać wykonane szybciej niż inicjalizacja tej zmiennej.

CoolMind
źródło