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ę iż 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 iż 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, iż 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” dzięki 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?
Pokaż odpowiedź
Dostaniemy albo wynik parsowania Int’a albo null, więc result będzie miała typ Int?.
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 przez cały czas będzie wiedział, iż ż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’emWspółpraca z Javą
Osobnym tematem jest zawsze kooperacja 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, iż 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 iż 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 – choćby 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 IOExceptionPodsumowanie
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żeli 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!