Dziś przyjrzymy się obsłudze wyjątków (ang. exception handling). Zobaczymy jak działa ona w Kotlinie, czym się różni od tej z Javy, i jakie ciekawostki i udogodnienia w sobie kryje.
Zapraszam do lektury 😉
Wstęp
Obsługa wyjątków występuje – w takiej czy innej formie – chyba w każdym nowożytnym języku programowania. Niezależnie czy kodujesz w Javie, C#, Swift’cie, JavaScript’cie czy Pythonie, na pewno z niej korzystasz. Myślę że sens jej istnienia również jest jasny – wyraźne oddzielenie przepływu informacji poprawnych od tych błędnych.
Model obsługi wyjątków Kotlina jest w gruncie rzeczy bardzo podobny do tego z Javy, a jeszcze bardziej z C#. Czym się różni jeden od drugiego? Przede wszystkim brakiem wyjątków sprawdzanych/przechwytywanych (ang. checked exceptions).
Wyjątki sprawdzane
Powiedzmy to sobie wyraźnie:
Kotlin nie posiada wyjątków sprawdzanych.
Wynika to z praktyki i doświadczenia innych języków. A jako że w temacie wypowiadali się już chyba wszyscy liczący się specjaliści, to ja nie będę się wymądrzał, tylko przytoczę kilka wypowiedzi 😉
Bruce Eckel (Thinking in Java) powiedział:
W Javie (…) wymyślono wyjątki sprawdzane. Stanowią one eksperyment, którego jak dotąd nie zdecydowali się powtórzyć twórcy żadnego innego języka.
Przytacza on również wypowiedź jednego z projektantów C#, który twierdzi że…
…doświadczenia z dużymi projektami wskazują (…) spadek efektywności i niewielką poprawę jakości lub jej całkowity brak.
Również Martin Fowler nie szczędzi krytyki:
…ogólnie uważam, że wyjątki są dobre, jednak sprawdzane wyjątki w Javie przysparzają więcej problemów niż korzyści…
throw, try, catch, finally…
Wyjątki “rzucamy” za pomocą throw
:
throw Exception("Something went wrong!")
Możemy tu użyć dowolnego wyjątku Javy, Kotlina, oraz oczywiście tworzyć własne klasy dziedziczące po Throwable
.
Wyjątki “łapiemy” standardowym blokiem try-catch-finally
:
try { // tutaj jest kod potencjalnie rzucający wyjątek } catch (e: SomeException) { // tutaj jest obsługa wyjątków } finally { // a tutaj czyszczenie zasobów }
Dozwolone są też “niepełne” formy, czyli try-catch
i try-finally
:
// brak bloku `finally` + wiele bloków `catch` try { // tutaj jest nasz kod } catch (e: SomeException) { // obsługa wyjątków SomeException } catch (e: SomeOtherException) { // obsługa wyjątków SomeOtherException } // brak bloku `catch` try { // tutaj jest nasz kod } finally { // czyszczenie zasobów }
W tym miejscu kończą się podobieństwa z Javą. W związku z brakiem wyjątków sprawdzanych, nie istnieje tutaj klauzula throws
. Natomiast Kotlin nie powiedział jeszcze ostatniego słowa…
try
może zwracać wartość
Tak samo jak if-else
, również try
jest Kotlinie wyrażeniem, i może zwracać wartość. Możemy więc napisać tak:
val result = try { parseInt(input) } catch (e: Exception) { null }
W takim wypadku zwrócone zostanie ostatnie wyrażenie z bloku try
, lub – w przypadku błędu – ostatnie wyrażenie z bloku catch
. Jaki typ posiada więc zmienna result
?
Również try-finally
może zwrócić wartość, i będzie nią ostatnie wyrażenie z bloku try
. Wobec czego możemy napisać coś takiego:
val newState = try { isDispatching = true reducer(currentState, action) } finally { isDispatching = false }
reducer
zwraca nowy stan na podstawie stanu aktualnego i akcji.
Typ Nothing
Kolejną ciekawostką jest fakt, iż throw
również może być użyte jako wyrażenie. Jego wynikiem jest zagadkowy typ Nothing
, o którym pisałem już tutaj (pkt. 5). Dzięki temu możemy na przykład tworzyć konstrukcje takie, jak ta poniżej:
val address = user.address ?: throw Exception("Address required")
Możemy również zamknąć wyrażenie throw
wewnątrz funkcji, i dzięki typowi Nothing
kompilator nadal będzie wiedział, że żaden kod po wywołaniu tej funkcji nie będzie już wykonany:
fun fail(message: String): Nothing { throw IllegalArgumentException(message) } ... val address = user.address ?: fail("Address required") println(address) //w tym miejscu `address` na pewno nie jest null’em
Współpraca z Javą
Osobnym tematem jest zawsze współpraca Kotlina z Javą. Nie inaczej jest tym razem. A kością niezgody są tutaj oczywiście… wyjątki sprawdzane 😉
Kiedy wołamy kod Javy z poziomu Kotlina, sytuacja jest prosta. Mając metodę Javową, która deklaruje, że zwraca wyjątek (poprzez klauzulę throws
), w Kotlinie widzimy ją jakby tego throws
w ogóle nie było.
Gorzej jest w drugą stronę. Załóżmy że mamy w Kotlinie funkcję, która potencjalnie rzuca wyjątek, a jej sygnatura wygląda tak:
fun foo() { throw IOException() }
W związku z brakiem klauzuli throws
, Java nie wie nic o tym wyjątku, i – co więcej – nawet nie pozwoli nam go obsłużyć:
// Java try { foo(); } catch (IOException e) { // error: foo() does not declare IOException in the throws list ... }
Na takie sytuacje mamy w Kotlinie specjalną adnotację @Throws
:
@Throws(IOException::class) fun foo() { throw IOException() }
Teraz Java będzie widzieć tę funkcję jako:
public void foo() throws IOException
Podsumowanie
Jak widzisz, model obsługi wyjątków Kotlina oparty jest na solidnych podstawach (C++, Java), a dodatkowo uczy się na błędach innych (brak wyjątków przechwytywanych), i – jak zwykle – wtrąca swoje “trzy grosze” (typ Nothing
, try
zwraca wartość).
A jeśli Ci się podobało, to poniżej masz kilka kolorowych przycisków – warto z nich skorzystać 😉
Z mojej strony to wszystko. Do następnego wpisu!
„Kotlin nie posiada wyjątków przechwytywanych.” chyba „… wyjątków weryfikowalnych/sprawdzanych”. Przechwycić w Java możemy jakikolwiek wyjątek.
Hej 🙂 Tu akurat sprawa nie jest taka oczywista. Ja się sugerowałem polskim tłumaczeniem książki „Java. Efektywne programowanie”, gdzie wyjątki są podzielone na „przechwytywane” i „czasu wykonania”. Z kolei „Thinking in Java” mówi o „sprawdzanych” i „nie-sprawdzanych”, a w „Czystym kodzie” piszą o „kontrolowanych” i „niekontrolowanych”. I bądź tu mądry… 😉 Ale spróbuję coś zrobić, żeby rozwiać tę niejasność.
Czy jest szansa że w najbliższym czasie rozwinie temat dotyczący korutyn w Kotlinie i ich praktycznego zastosowania ? 😛