Kotlin – Obsługa Wyjątków

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?

Pokaż odpowiedź

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!

 

Nie przegap kolejnych wpisów - subskrybuj!

2 komentarzy

  1. „Kotlin nie posiada wyjątków przechwytywanych.” chyba „… wyjątków weryfikowalnych/sprawdzanych”. Przechwycić w Java możemy jakikolwiek wyjątek.

    1. 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ść.

Dodaj komentarz