1. finally: «прибирання», яке станеться за будь-якого сценарію
Коли ви пишете програму, майже завжди подумки очікуєте «ідеального світу»: користувач вводить коректні дані, обчислення спрацьовують, ми друкуємо результат і красиво йдемо зі сцени під оплески. Але реальний світ — це коли користувач вводить -999, потім q, потім 2147483648, а тоді ще й ображається, що програма «не розуміє простих речей». Блок finally саме про те, щоб ваш код коректно завершував важливі дії незалежно від того, сталася помилка чи ні.
Формально finally { ... } — це блок, який виконується завжди: і якщо try відпрацював успішно, і якщо всередині try стався виняток, і навіть якщо виняток перехопили в catch. Це класична конструкція try/catch/finally. У Kotlin finally часто описують як блок для clean-up — тобто для дій «прибирання», які потрібно виконати в будь-якому разі.
Уявіть аналогію: ви готуєте на кухні. Неважливо, вдалася страва чи ви випадково спалили макарони — посуд мити все одно доведеться. Ось finally — це «помити посуд», а не «виправити макарони».
Мініскелет try/catch/finally
fun main() {
try {
println("try: робимо щось корисне")
println(10 / 2) // 5
} catch (e: ArithmeticException) {
println("catch: помилка ділення")
} finally {
println("finally: я виконаюся завжди")
}
}
Якщо замінити 10 / 2 на 10 / 0, рядок із catch виконається, але рядок із finally теж виконається.
try { ... } finally { ... } без catch
Іноді вам не потрібно обробляти помилку прямо тут. Потрібне інше: гарантовано завершити дію, а далі нехай помилка «підіймається вгору» — і програма падає (або виняток перехоплюється десь вище). Звучить суворо, але на практиці це буває корисно навіть у невеликих консольних програмах. Наприклад, ви хочете наприкінці завжди надрукувати «Сеанс завершено», скинути тимчасовий стан або закрити якийсь ресурс.
Kotlin дозволяє використовувати try з одним finally без catch. При цьому finally виконається, а виняток — якщо він стався — продовжить «жити своїм життям» (тобто програма все одно впаде, якщо його ніхто не перехопив). Kotlin також підкреслює: try має супроводжуватися принаймні catch або finally — «самотній try» не має сенсу.
fun main() {
println("Старт")
try {
val a = 10
val b = 0
println(a / b) // ArithmeticException
} finally {
println("Завершення: дякуємо, що протестували програму")
}
println("Фініш") // не виконається
}
Зверніть увагу на важливу деталь: finally виконається, а рядок println("Фініш") — ні. Тобто finally не «зцілює» програму. Він лише гарантує, що ви встигнете виконати потрібну завершальну дію.
finally і return: finally виконується, але результат не «переписує»
На перших порах finally може здаватися магією: «Він завжди виконується, отже, може все змінити?» Ні: роль finally інша. Kotlin окремо підкреслює, що finally завжди виконується, але не змінює результат try/catch-виразу (що повернеться з try або catch, те й буде результатом).
Це важливо, бо дуже скоро ви почнете писати функції, які повертають значення з try/catch. І вам потрібно чітко розуміти модель: finally — це «побічний завершальний ритуал», а не «ще один спосіб повернути значення».
fun divideOrMinusOne(a: Int, b: Int): Int {
try {
return a / b
} catch (e: ArithmeticException) {
return -1
} finally {
println("finally: спробу ділення завершено")
}
}
fun main() {
println(divideOrMinusOne(10, 2)) // finally: спробу ділення завершено
// 5
println(divideOrMinusOne(10, 0)) // finally: спробу ділення завершено
// -1
}
Якщо ви вперше бачите return усередині try/catch/finally, не лякайтеся. Логіка проста: щойно визначено, що саме повертати, Kotlin усе одно виконує finally перед виходом із функції.
2. throw: як чесно сказати «так не можна»
До цього моменту винятки поводилися як «стихійне лихо»: ділення на нуль, погане введення — і програма падає. Але розробник — істота хитра: іноді ми самі хочемо сказати «це заборонено правилами нашої програми» й зупинити виконання, щоб не продовжувати в неправильному стані.
Для цього є ключове слово throw: воно явно викидає виняток. Kotlin показує це так само прямо, як і в інших мовах: throw IllegalArgumentException("...").
Корисна модель для новачка: throw — це не «повідомлення про помилку», а аварійна зупинка. Ви не просите програму «будь ласка, будь обережнішою». Ви кажете: «Далі їхати не можна: міст закінчився».
Приклад: функція приймає тільки додатне число
fun parsePositiveInt(text: String): Int {
val n = text.toInt()
if (n <= 0) {
throw IllegalArgumentException("Очікувалося число > 0, але надійшло: $n")
}
return n
}
Тут ми не «ловимо» помилку. Ми формуємо контракт функції: вона повертає Int, але лише якщо число додатне. Якщо ні — це не «звичайний сценарій», а порушення правил використання функції.
Важливий нюанс: throw — вираз типу Nothing
Це звучить страшніше, ніж є насправді. Тип Nothing ви вже зустрічали раніше: він означає «цей вираз ніколи не повертає значення». Довідка Kotlin показує класичний патерн, де throw зручно використовувати як «жорсткий запасний варіант» в Elvis-операторі ?:.
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
fun main() {
val name: String? = null
val nonNullName = name ?: fail("Імʼя обовʼязкове")
println(nonNullName) // сюди не дійдемо
}
Поки що сприймайте це як приємний бонус: компілятор розуміє, що після fail(...) виконання не продовжується, і тому типи «збігаються».
Як вибрати виняток і написати повідомлення
Коли ви вперше дізнаєтеся про throw, зʼявляється спокуса: «О! Буду кидати винятки взагалі всюди». Не поспішайте перетворювати код на мінне поле. throw доречний там, де ви хочете захистити зміст функції: якщо вхідні дані не підходять, краще зупинитися й сказати про це явно.
У навчальних задачах і невеликих консольних програмах найчастіше використовують IllegalArgumentException: цей виняток буквально означає «аргумент функції неправильний». Kotlin активно використовує цей тип у прикладах про throw.
Повідомлення винятку — це ваша записка самому собі (і майбутньому вам). Хороше повідомлення зазвичай містить дві частини: що очікувалося і що надійшло. Тоді під час падіння не доведеться гадати, чому програма вирішила завершитися.
fun divideStrict(a: Int, b: Int): Int {
if (b == 0) {
throw IllegalArgumentException("Дільник не може бути 0 (a=$a, b=$b)")
}
return a / b
}
Так, можна було б просто спробувати поділити й перехопити ArithmeticException. Але інколи краще зробити перевірку й викинути свій зрозумілий виняток. Тут немає єдиної правильної відповіді — важливо, щоб поведінка була зрозумілою.
3. Міні застосунок: ділення зі зрозумілою поведінкою
Зараз буде найпрактичніша частина: ми зберемо маленький консольний застосунок, який робить одну дію (ділення), але робить це по-дорослому. Він акуратно читає введення, перевіряє правила, повідомляє людині, що не так, і завжди друкує фінальний рядок наприкінці спроби. Такий стиль згодом дуже знадобиться, коли ви почнете писати функції введення та валідації (так, це буде окремий рівень курсу — сьогодні лише фундамент).
Крок 1: читаємо число або null
fun readIntOrNull(prompt: String): Int? {
print(prompt)
val raw = readln().trim()
return raw.toIntOrNull()
}
Тут нічого чарівного: trim() прибирає зайві пробіли, а toIntOrNull() або повертає Int, або null.
Крок 2: строге ділення з throw
fun divideStrict(a: Int, b: Int): Int {
if (b == 0) throw IllegalArgumentException("Ділити на 0 не можна")
return a / b
}
Це наш «контракт»: дільник 0 заборонений правилами, тож ми прямо кажемо: «так не можна».
Крок 3: main — перехоплюємо очікувану помилку й завжди завершуємо
fun main() {
val a = readIntOrNull("Введіть a: ")
val b = readIntOrNull("Введіть b: ")
try {
if (a == null || b == null) throw IllegalArgumentException("Потрібно ввести два цілих числа")
val result = divideStrict(a, b)
println("a / b = $result") // наприклад: a / b = 5
} catch (e: IllegalArgumentException) {
println("Помилка введення/правил: ${e.message}")
} finally {
println("Готово: спробу обчислення завершено")
}
}
Що тут важливо помітити (і це справді ключова навичка цього рівня):
try містить лише те, що реально може «зламатися» в межах нашої логіки: перевірку правил, виклик divideStrict(...), друк результату. Якщо все пройшло добре, catch пропускається, але finally все одно виконується. Якщо ми кинули IllegalArgumentException (самі або з divideStrict), керування переходить у catch, а потім усе одно виконується finally.
Схема потоку виконання
Коли ви лише починаєте, try/catch/finally легко перетворюються на кашу в голові: «куди стрибає керування і що виконується?». Тут допомагає проста блок-схема. Уявіть, що програма — це потяг, а виняток — екстрене гальмо.
flowchart TD
A[Входимо в try] --> B{У try стався виняток?}
B -- ні --> C[Виконуємо код try до кінця]
B -- так --> D[Стрибаємо в catch відповідного типу]
C --> E[Виконуємо finally]
D --> E[Виконуємо finally]
E --> F{Виняток оброблено?}
F -- так --> G[Продовжуємо після try/catch/finally]
F -- ні --> H["Виняток 'злітає' вище, програма може впасти"]
Ця схема не про «нутрощі JVM», а про вашу щоденну практику: розуміти, чому наступний рядок не виконується, зате finally виконується.
4. Типові помилки під час роботи з finally і throw
Помилка №1: намагатися «обробляти» помилку у finally.
Іноді новачок думає: раз finally виконується завжди, то туди треба покласти «логіку порятунку». Але finally — для іншого: там не лагодять проблему, там «прибирають». Якщо ви у finally починаєте розбирати введення або змінювати важливі дані, код стає непередбачуваним: помилка вже сталася, а ви додаєте ще один шар сюрпризів.
Помилка №2: писати у finally код, який і сам може впасти.
Звучить як жарт, але на практиці таке трапляється постійно: у finally щось друкують, звертаються до змінної, яка могла не ініціалізуватися, або роблять ділення «для логу». У підсумку ви втрачаєте початкову причину помилки, бо поверх неї накладається нова. Хороший finally зазвичай короткий, простий і максимально надійний.
Помилка №3: використовувати throw замість звичайної перевірки сценарію користувача.
Якщо користувач увів щось не те, це найчастіше очікувана ситуація. Її можна обробити мʼяко: вивести повідомлення й попросити повторити введення. throw корисніший для порушення правил функцій (контрактів), коли продовжувати виконання справді небезпечно. На нашому рівні нормально кинути виняток для «ділити на нуль не можна», але якщо ви почнете кидати винятки на кожну описку — програма постійно «вибухатиме».
Помилка №4: кидати виняток без нормального повідомлення.
Виняток без повідомлення — як записка «все погано» без уточнень. Kotlin дозволяє кинути IllegalArgumentException() і без тексту, але тоді під час налагодження ви отримаєте менше підказок. Набагато краще писати конкретно: що очікували й що отримали. В офіційних прикладах Kotlin це роблять саме так.
Помилка №5: думати, що finally «продовжить програму» після падіння.
finally виконається, так. Але якщо виняток не перехоплено (або ви використовуєте try/finally без catch), то після finally виконання не повертається до нормального сценарію: виняток усе одно піде вище. Kotlin показує, що finally гарантує виконання завершального коду, але не обіцяє, що програма піде далі, якщо помилку не оброблено.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ