Kotlin – Obsługa Wyjątków

blog.geekydevs.com 6 lat temu

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’em

Współ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 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ż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!

Idź do oryginalnego materiału