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 ? 😛