Instrukcje warunkowe w Kotlinie
To jest rozdział z książki Kotlin Essentials.
Większość instrukcji warunkowych, takich jak warunek if czy pętla while, wygląda tak samo w Kotlinie, Javie, C++, JavaScript i w większości innych nowoczesnych języków. Dla przykładu instrukcja if jest identyczna we wszystkich tych językach:
Jednak instrukcja if w Kotlinie jest bardziej zaawansowana i ma możliwości, których nie obsługują poprzednicy Kotlina. Zakładam, że czytelnicy tej książki mają ogólne doświadczenie w programowaniu, więc skoncentruję się na różnicach, które Kotlin wprowadził w porównaniu z innymi językami programowania.
Instrukcja if
Zacznijmy od wspomnianej instrukcji if. Wykonuje ona swoje ciało, gdy jej warunek jest spełniony (zwraca true
). Możemy dodatkowo dodać blok else
, który jest wykonywany, gdy warunek nie jest spełniony (zwraca false
).
Jedną z supermocy Kotlina jest to, że instrukcja if-else może być używana jako wyrażenie1, a więc do zwrócenia wyniku wybranego ciała.
Jaka wartość zostanie zwrócona? Dla każdego bloku ciała jest to wynik ostatniego wyrażenia (lub Unit
dla pustego ciała lub instrukcji niezwracającej wartości2).
Gdy ciało ma tylko jedno wyrażenie, jego wynik jest wynikiem naszego wyrażenia if-else. W takim przypadku nie potrzebujemy nawiasów.
Sposób użycia instrukcji if-statement jest alternatywą w Kotlinie dla operatora trójargumentowego używanego w Java lub JavaScript.
// Java
final String name = user == null ? "" : user.name
// JavaScript
const name = user === null ? "" : user.name
Należy zaznaczyć, że if-else jest dłuższe niż składnia operatora trójargumentowego. Wierzę, że jest to główny powód, dla którego niektórzy deweloperzy chcieliby wprowadzenia operatora trójargumentowego w Kotlinie. Jednak jestem temu przeciwny, ponieważ if-else jest bardziej czytelne i może być lepiej sformatowane. Co więcej, mamy w Kotlinie inne narzędzia będące zamiennikami dla niektórych przypadków użycia operatora trójargumentowego: operator Elvisa, rozszerzenia dla typów nullowalnych (takie jak orEmpty
) czy safe-call. Wszystkie te elementy zostaną wyjaśnione szczegółowo w rozdziale Nullability.
// Java
String name = user == null ? "" : user.name
Zauważ, że jeśli używasz tzw. instrukcji if-else-if, są to po prostu wielokrotnie połączone instrukcje if-else.
W rzeczywistości nie ma czegoś takiego jak wyrażenie if-else-if, to po prostu jedno wyrażenie if-else umieszczone wewnątrz innego. Można się o tym przekonać w pewnych nietypowych przypadkach, na przykład, gdy na całym wyrażeniu if-else-if wykonywana jest jakaś metoda. Spójrz na poniższą zagadkę i spróbuj przewidzieć wynik tego kodu.
Odpowiedź nie brzmi "ujemna,zero,dodatnia", ponieważ nie ma czegoś takiego jak pojedyncze wyrażenie if-else-if (tylko dwie zagnieżdżone instrukcje if-else). W związku z tym powyższa implementacja printNumberSign
daje taki sam wynik jak poniższa implementacja.
Więc, gdy wywołujemy metodę print
na wyniku, jest ona wywoływana tylko na wyniku drugiego wyrażenia if-else (tego z "dodatnia" i "zero"). Oznacza to, że powyższy kod wydrukuje ",zero,dodatnia". Jak możemy to naprawić? Moglibyśmy użyć nawiasów, ale zazwyczaj zaleca się, aby zamiast używać if-else-if, używać instrukcji when, gdy mamy więcej niż jeden warunek. Może to pomóc uniknąć błędów, takich jak ten w powyższej zagadce, i sprawia, że kod jest bardziej przejrzysty i łatwiejszy do odczytania.
Instrukcja when
Instrukcja when jest alternatywą dla if-else-if. W każdej jego gałęzi określamy predykat i ciało, które powinno zostać wykonane, jeśli dany predykat zwróci true
(a wcześniejsze predykaty nie). Działa więc tak samo, jak if-else-if, ale jest preferowana ze względu na składnię dostosowaną do wielu warunków.
Podobnie jak w przypadku instrukcji if, nawiasy klamrowe są potrzebne tylko dla ciał z więcej niż jednym poleceniem.
Instrukcja when
może być również używana jako wyrażenie, ponieważ może zwracać wartość. Wynikiem jest ostatnie wyrażenie wybranej gałęzi, dlatego poniższy przykład wydrukuje "Prawdopodobnie".
Instrukcja when często używana jest jako wyrażenie stanowiące ciało funkcji z pojedynczym wyrażeniem3:
Instrukcja when z wartością
Istnieje także inna forma instrukcji when. Jeśli dodamy wartość w nawiasach po słowie kluczowym when
, nasza instrukcja when staje się alternatywą dla switch-case. Jest jednak znacznie potężniejsza alternatywa, ponieważ może nie tylko porównywać wartości pod względem równości, ale także sprawdzać, czy obiekt jest danego typu (używając is
) lub czy obiekt zawiera tę wartość (używając in
). Każdy blok może mieć wiele wartości, z którymi porównujemy, oddzielonych przecinkami.
Instrukcja when
z wartością może być również używana jako wyrażenie, a więc zwracać wartość:
Należy zauważyć, że jeśli używamy when jako wyrażenia, jego warunki muszą być wyczerpujące: powinny obejmować wszystkie możliwości lub dostarczyć gałąź else, jak w powyższym przykładzie. Jeśli nie są spełnione wszystkie warunki, pokazany jest błąd kompilatora.
Kotlin nie obsługuje instrukcji switch-case, ponieważ zamiast tego używamy instrukcji when.
W nawiasach "when", gdzie określamy wartość, możemy również zdefiniować zmienną, a jej wartość zostanie użyta w każdym warunku.
Sprawdzanie is
Skoro już wspomnieliśmy o operatorze is
, omówmy go nieco dokładniej. Sprawdza on, czy dana wartość jest określonego typu. Wiemy już, że 123
jest typu Int
, a "ABC"
jest typu String
. Z pewnością 123
nie jest typu String
, a "ABC"
nie jest typu Int
. Możemy to potwierdzić, używając sprawdzenia is
.
Zauważ, że 123
jest Int
, ale jest również Number
; sprawdzenie is
zwraca true
dla obu tych typów.
Kiedy chcemy sprawdzić, czy wartość nie jest określonego typu, możemy użyć !is
; jest to odpowiednik sprawdzenia is, po czym zanegowania wyniku.
Rzutowanie
Zawsze można użyć wartości, której typem jest Int
, jako Number
, ponieważ każdy Int
jest Number
. Ten proces nazywa się rzutowaniem w górę, ponieważ zmieniamy typ wartości z niższego (bardziej konkretnego) na wyższy (mniej konkretny).
Możemy rzutować niejawne z niższego typu na wyższy, ale nie na odwrót. Każdy Int
jest Number
, ale nie każdy Number
jest Int
, ponieważ istnieje więcej podtypów Number
, takich jak Double
czy Long
. Dlatego nie możemy użyć Number
, gdzie oczekiwany jest Int
. Jednak czasami mamy sytuację, gdy jesteśmy pewni, że wartość jest określonego typu, nawet jeśli używany jest jej nadtyp. Jawna zmiana z wyższego typu na niższy nazywa się rzutowaniem w dół i wymaga operatora as
w Kotlinie.
Ogólnie unikamy używania as
, gdy nie jest ono konieczne, ponieważ uważamy, że rzutowanie w dół jest mało bezpieczne. Rozważ powyższy przykład. Co jeśli ktoś zmieni 123
na 3.14
? Obie wartości są typu Number
, więc kod będzie kompilować się bez żadnych problemów czy ostrzeżeń. Ale 3.14
to Double
, a nie Int
, i rzutowanie nie jest możliwe! W związku z tym powyższy kod zakończy się błędem z wyjątkiem ClassCastException
.
Istnieją dwa popularne sposoby radzenia sobie z tego typu problemami. Pierwszy to użycie jednej z funkcjonalności Kotlina do bezpiecznej konwersji wartości. Jednym z przykładów jest użycie smart castingu, które zostanie opisane w kolejnej sekcji. Innym przykładem jest funkcja konwersji, taka jak metoda toInt
, która przekształca Number
na Int
(i ewentualnie traci część dziesiętną).
Drugą opcją jest operator as?
, który zamiast rzucać wyjątkiem, zwraca null
, gdy rzutowanie nie jest możliwe. Omówimy obsługę wartości nullowalnych później.
W Kotlinie uważamy as?
za bezpieczniejszą opcję niż as
, ale zbyt częste używanie obu tych operatorów jest uważane za code smell4. Opiszmy smart casting, który jest ich popularną alternatywą.
Smart casting
Kotlin ma potężną funkcjonalność o nazwie smart casting, która pozwala na automatyczne rzutowanie typów, gdy kompilator może być pewien, że zmienna jest określonego typu. Spójrz na poniższy przykład:
Funkcja convertToInt
konwertuje argument typu Number
na Int
w następujący sposób: jeśli argument jest już typu Int
, jest on zwracany; w przeciwnym razie jest konwertowany za pomocą metody toInt
. Zauważ, że aby ten kod się skompilował, num
w pierwszym ciele warunku musi być typu Int
. W większości języków taka wartość musiałaby być rzutowana, ale w Kotlinie dzieje się to automatycznie. Spójrz na kolejny przykład:
Wewnątrz predykatu warunku if sprawdziliśmy, czy a
jest typu String
. Ciało tego wyrażenia zostanie wykonane tylko wtedy, gdy sprawdzenie typu zakończy się powodzeniem. Dlatego wewnątrz tego bloku kompilator uznaje, że a
jest typu String
, dlatego możemy sprawdzić długość tekstu. Taka konwersja, z Any
na String
, jest wykonana niejawnie przez kompilator Kotlina. Może się to zdarzyć tylko wtedy, gdy Kotlin jest pewien, że żaden inny wątek nie może zmienić naszej właściwości, więc kiedy jest to stała lub zmienna lokalna. Nie zadziała to dla nielokalnych właściwości var
, ponieważ w takich przypadkach nie ma gwarancji, że nie zostały one zmodyfikowane między sprawdzeniem a użyciem (np. przez inny wątek).
Smart casting jest często używany razem z instrukcją when. Kiedy są używane razem, czasami nazywane są Kotlinowym pattern-matchingiem. Więcej przykładów zostanie przedstawionych, gdy omówimy modyfikator sealed
.
Pętle while i do-while
Ostatnimi strukturami sterującymi, o których musimy wspomnieć, są pętle while i do-while. Wyglądają i działają dokładnie tak samo jak w Java, C++ i wielu innych językach.
Mam nadzieję, że nie potrzebują one dalszego wyjaśnienia. Pętle while i do-while nie mogą być używane jako wyrażenia. Dodam tylko, że zarówno pętle while, jak i do-while są rzadko używane w Kotlinie. Zamiast tego używamy funkcji przetwarzania kolekcji lub sekwencji, które zostaną omówione w książce Funkcyjny Kotlin. Na przykład powyższy kod można zastąpić następującym:
Podsumowanie
Jak widać, Kotlin wprowadził wiele potężnych funkcji do instrukcji warunkowych. Warunki if i when mogą być używane jako wyrażenia. Instrukcja when jest bardziej zaawansowaną alternatywą dla if-else-if lub switch-case. Obsługiwane jest sprawdzanie typów ze smart castingiem. To czyni instrukcje warunkowe potężniejszymi niż w innych językach. Teraz zobaczmy, co Kotlin zmienił w funkcjach.
"Wyrażenie" w programowaniu to część kodu, która zwraca wartość.
Unit
to obiekt używany by sygnalizować brak istotnej wartości. Przypomina Void
w Javie.
Funkcja z pojedynczym wyrażeniem to specjalna składnia do implementacji ciał funkcji z jednym wyrażeniem. Zostanie ona omówiona w kolejnym rozdziale.
Terminu "code smell" będę używał do opisania praktyk, które nie są wyraźnie błędne, ale uważa się, że powinno się ich unikać.