Nullowalność w Kotlinie
Kotlin pierwotnie powstawał jako rozwiązanie problemów Javy, a największym problemem w Javie jest możliwość wystąpienia wartości null
niemalże wszędzie. W Javie, jak w wielu innych językach, każda zmienna może przyjmować wartość null
. Każde wywołanie na wartości null
prowadzi do słynnego NullPointerException
(NPE). Jest to najpopularniejszy wyjątek w większości projektów napisanych w Javie0. Jest tak powszechny, że często nazywany jest "błędem za miliard dolarów" po słynnym przemówieniu Sir Charlesa Antony'ego Richarda Hoare'a, który powiedział: "Nazywam to moim błędem za miliard dolarów. Był to wynalazek null reference w 1965 roku... Doprowadziło to do niezliczonych błędów, podatności i awarii systemów, które przez ostatnie czterdzieści lat prawdopodobnie spowodowały straty rzędu miliarda dolarów".
Jednym z priorytetów Kotlina było ostateczne rozwiązanie tego problemu, co udało się doskonale. Wprowadzone w nim mechanizmy są tak skuteczne, że zobaczenie NullPointerException
rzuconego z kodu napisanego w Kotlinie jest niezwykle rzadkie. Wartość null
przestała być problemem, a programiści Kotlina już się jej nie obawiają. Stała się naszym przyjacielem.
Jak to zatem działa? Mechanizm nullowalności w Kotlinie opiera się na następujących zasadach:
- Każda zmienna musi mieć określoną wartość. Nie ma czegoś takiego jak domyślna wartość
null
.
- Zwykły typ nie akceptuje wartości
null
.
- Aby określić typ nullowalny, musisz zakończyć zwykły typ znakiem zapytania (
?
). W ten sposób tworzysz typ nullowalny, który może przyjmować wartośćnull
.
- Zmienne nullowalne nie mogą być używane bezpośrednio. Muszą być używane bezpiecznie lub wcześniej rzutowane za pomocą jednego z narzędzi przedstawionych później w tym rozdziale.
Dzięki wszystkim tym mechanizmom zawsze wiemy, co może być null
, a co nie. Dzięki temu używamy typów nullowalnych i wartości null
tylko wtedy, gdy tego potrzebujemy, czyli gdy istnieje ku temu jakiś powód. W takich przypadkach programiści są zmuszeni do jawnej obsługi wartości null
. We wszystkich innych przypadkach nie ma takiej potrzeby. Jest to doskonałe rozwiązanie, ale potrzebne są dobre narzędzia, aby poradzić sobie z nullowalnością w sposób wygodny dla programistów. Kotlin zapewnia takie narzędzia, w tym bezpieczne wywołania, asercje not-null, smart casting czy operator Elvisa. Omówmy je po kolei.
Bezpieczne wywołania
Najprostszym sposobem wywołania metody lub właściwości na wartości nullowalnej jest bezpieczne wywołanie, które polega na użyciu znaku zapytania i kropki (?.
) zamiast zwykłej kropki (.
). Bezpieczne wywołania działają następująco:
- jeśli wartość to
null
, nic nie robi i zwracanull
, - jeśli wartość nie jest
null
, działa jak zwykłe wywołanie.
Zauważ, że wynik bezpiecznego wywołania zawsze ma typ nullowalny, ponieważ bezpieczne wywołanie zwraca null
, gdy jest wywołane na null
. Oznacza to, że wartość null
propaguje się. Jeśli chcesz dowiedzieć się, jaką długość ma imię użytkownika, wywołanie user?.name.length
się nie skompiluje. Mimo że name
nie jest wartością nullowalną, wynik user?.name
to String?
, więc musimy ponownie użyć bezpiecznego wywołania: user?.name?.length
.
Asercja not-null
Kiedy nie spodziewamy się wartości null
i chcemy rzucić wyjątkiem, gdy wystąpi, możemy użyć asercji not-null !!
.
To nie jest bardzo bezpieczna opcja, ponieważ jeśli się mylimy i wartość null
znajduje się tam, gdzie jej nie oczekujemy, prowadzi to do wyjątku NullPointerException
. Dlatego stosujemy !!
tylko wtedy, gdy rzeczywiście chcemy zobaczyć wyjątek, jeśli wartość jest null
. Aczkolwiek w takich przypadkach częściej wolimy użyć funkcji, które rzucają bardziej znaczący wyjątek, jak requireNotNull
lub checkNotNull
4:
requireNotNull
, który przyjmuje wartość nullowalną jako argument i rzuca wyjątekIllegalArgumentException
, jeśli ta wartość jest równanull
. W przeciwnym razie zwraca tę wartość jako nienullowalną.checkNotNull
, który przyjmuje wartość nullowalną jako argument i rzuca wyjątekIllegalStateException
, jeśli ta wartość jest równanull
. W przeciwnym razie zwraca tę wartość jako nienullowalną.
Smart casting
Smart casting działa również dla wartości null
. W związku z tym, w zakresie, w którym pewne jest, że wartość nie jest null
, typ nullowalny jest rzutowany na typ nienullowalny.
Smart casting działa również, gdy używamy return
lub throw
.
Smart casting jest dość inteligentny i działa w różnych przypadkach, takich jak po &&
i ||
w wyrażeniach logicznych.
Smart casting działa dla
requireNotNull
dzięki Kotlinowym kontraktom, które opisałem w książce Zaawansowany Kotlin.
Operator Elvisa
Ostatnią specjalną funkcją Kotlina, która jest używana do obsługi wartości null
, jest operator Elvisa ?:
. Tak, to znak zapytania i dwukropek. Nazywa się go operatorem Elvisa, ponieważ przypomina Elvisa Presleya (z jego charakterystyczną fryzurą). Możesz sobie wyobrazić, że patrzy na nas zza muru, więc widzimy tylko jego włosy i oczy.
Operator Elvisa umieszcza się pomiędzy dwiema wartościami. Jeśli wartość po lewej stronie operatora Elvisa nie jest null
, to jest ona wynikiem. Jeśli lewa strona jest null
, zwracana jest prawa strona.
Używamy operatora Elvisa do określenia wartości domyślnej dla wartości nullowalnej.
Rozszerzenia dla typów nullowanych
Zwykłe funkcje nie mogą być wywoływane na wartościach nullowalnych. Istnieje jednak specjalny rodzaj funkcji, funkcje rozszerzające, który można zdefiniować tak, aby można go było wywoływać na zmiennych nullowalnych3. Biblioteka standardowa Kotlina definiuje następujące funkcje, które można wywoływać na String?
:
orEmpty
zwraca pusty string zamiastnull
.isNullOrEmpty
zwracatrue
, jeśli wartość tonull
lub pusty string. W przeciwnym razie zwracafalse
.isNullOrBlank
zwracatrue
, jeśli wartość tonull
lub string nie zawierający żadnego nie-białego znaku. W przeciwnym razie zwracafalse
.
Biblioteka standardowa Kotlina zawiera również podobne funkcje dla nullowalnych list:
orEmpty
zwraca pustą listę zamiastnull
.isNullOrEmpty
zwracatrue
, jeśli wartość jestnull
lub pusta. W przeciwnym razie zwracafalse
.
Te funkcje pomagają nam operować na wartościach nullowanych.
null
to nasz przyjaciel
To, że każda wartość może być nullem, jest ogromnym problemem w językach takich jak Java. W efekcie programiści zaczęli unikać wartości null
i traktować ją jak wroga. Wiele książek dotyczących dobrych praktyk programowania w Javie sugeruje unikanie wartości null
, na przykład poprzez zastąpienie ich pustymi listami, stringami lub specjalną wartością enuma (patrz "Temat 43. Zwracanie pustych tablic lub kolekcji zamiast wartości null" z bardzo popularnej książki Java. Efektywne programowanie. Edycja druga autorstwa Joshua Blocha). Takie praktyki nie mają sensu w Kotlinie, gdzie mamy dobre wsparcie nullowalności i nie powinniśmy obawiać się wartości null
. W Kotlinie traktujemy null
jako naszego przyjaciela, a nie jako błąd1. Rozważ funkcję getAvailableUsers
. Istnieje istotna różnica między zwracaniem pustej listy a null
. Pusta lista powinna być interpretowana jako "wynik to pusta lista użytkowników, ponieważ żadni nie są dostępni". Wynik null
powinien być interpretowany jako "nie można wyprodukować wyniku, a lista użytkowników pozostaje nieznana". Zapomnij o przestarzałych praktykach dotyczących nullowalności. W Kotlinie wartość null
to nasz przyjaciel2.
Właściwości lateinit
Są sytuacje, gdy chcemy, aby właściwość klasy miała typ nienullowalny, ale nie możemy określić jej wartości podczas tworzenia obiektu. Chodzi przede wszystkim o właściwości, których wartość jest wstrzykiwana lub są tworzone w jednej z pierwszych metod cyklu życia klasy. Uczynienie takich właściwości nullowalnymi wymagałoby ich odpakowania przy każdym użyciu, mimo iż wiedzielibyśmy, że nie mogą mieć one wartości null
, ponieważ spodziewamy się, że ich wartość zostanie wstrzyknięta lub ustawiona odpowiednio wcześniej. Dla takich sytuacji twórcy Kotlina wprowadzili modyfikator lateinit
. Gdy go używamy, właściwość nie określa wartości pierwotnej, a przy tym musi mieć typ nienullowany. Kotlin spodziewa się, że ustawimy wartość tej właściwości przed jej pierwszym użyciem. Oto kilka praktycznych przykładów użycia lateinit
z Androida oraz testów jednostkowych w Kotlinie:
Gdy używamy właściwości lateinit, musimy ustawić jej wartość przed jej pierwszym użyciem. Jeśli tego nie zrobimy, program rzuci wyjątek UninitializedPropertyAccessException
w czasie wykonywania.
Zawsze możesz sprawdzić, czy właściwość została zainicjowana, używając właściwości isInitialized
na jej referencji. Aby użyć referencji do właściwości, użyj dwóch dwukropków i nazwy właściwości5.
Podsumowanie
Kotlin oferuje potężne wsparcie dla nullowalności, które sprawia, że null
przestaje być zagrożeniem, a staje się bezpieczny i prawdziwie użyteczny. System wsparcia obejmuje system typów, który rozróżnia co jest nullowalne, a co nie. Zmienne, które są nullowanye, muszą być używane bezpiecznie; do tego możemy użyć bezpiecznych wywołań, asercji not-null, smart castingu czy operatora Elvisa. Teraz przejdźmy wreszcie do klas. Używaliśmy ich już wiele razy, ale dopiero teraz mamy wszystko, czego potrzebujemy, aby dobrze je omówić.
Na przykład badanie OverOps potwierdza, że NullPointerException
jest najczęstszym wyjątkiem w 70% projektów Java.
Zobacz artykuł "Null is your friend, not a mistake" (link kt.academy/l/re-null) autorstwa Romana Elizarova, obecnego kierownika zespołu tworzącego język Kotlin.
Więcej informacji na temat stosowania nullowalności znajdziesz w książce Efektywny Kotlin.
Są to funkcje rozszerzające, które omówimy w rozdziale Rozszerzenia.
Więcej informacji na temat wyjątków IllegalArgumentException
i IllegalStateException
znajdziesz w rozdziale Wyjątki.
Aby odnieść się do właściwości innego obiektu, musimy zacząć od obiektu, zanim użyjemy ::
i nazwy właściwości. Więcej informacji na temat referencji do właściwości znajdziesz w książce Funkcyjny Kotlin oraz Zaawansowany Kotlin.