JavaRush /Курси /Kotlin SELF /Як завершувати функції в Kotlin: return

Як завершувати функції в Kotlin: return

Kotlin SELF
Рівень 8 , Лекція 2
Відкрита

1. Навіщо потрібен return, якщо можна «просто написати код»

Якщо ви пишете весь код у main, це схоже на історію без крапок і абзаців: загалом наче зрозуміло, але читати таке швидко втомлює. До того ж складно знайти місце, де «все пішло не так». Функція — це міні-історія, а return — крапка, яка завершує думку й віддає результат назовні. Без return обчислювальна функція або не скомпілюється, або поводитиметься не так, як ви очікуєте.

Уявіть життєву ситуацію: ви просите друга: «порахуй, скільки решти мені мають дати з 1000 USDT». Друг може: (1) відповісти числом («220»), або (2) просто виконати дію (наприклад, записати вам це число на папірці), або (3) піти в нескінченні роздуми про сенс грошей. У Kotlin це приблизно Int, Unit і Nothing.

2. return і тип результату функції

Коли ми пишемо fun sum(a: Int, b: Int): Int, ми обіцяємо: «виклик цієї функції дасть Int». Цю обіцянку перевіряє компілятор. І він доволі суворий, адже інакше ви отримували б «цікаві» сюрпризи.

Тому важливе правило сьогоднішньої лекції звучить так: якщо в сигнатурі вказано тип результату (наприклад, : Int), то на будь-якому шляху виконання функція має повернути значення цього типу.

На практиці це означає таке: ви можете вийти з функції раніше; можете мати if/else; можете мати цикл. Але компілятор має «бачити», що зрештою ви точно повертаєте Int. Інакше він скаже щось на кшталт «не можу довести, що ви повертаєте значення» — і матиме рацію.

Невеликий приклад правильного підходу:

fun sum(a: Int, b: Int): Int {
    return a + b
}

fun main() {
    val r = sum(10, 20)
    println(r) // 30
}

А ось приклад, який не скомпілюється, бо одна гілка нічого не повертає:

fun absBad(x: Int): Int {
    if (x < 0) {
        return -x
    }
    // Тут компілятор запитає: а що повертати, якщо x >= 0?
}

Правильна версія з явним поверненням на всіх шляхах розгалуження:

fun absInt(x: Int): Int {
    if (x < 0) {
        return -x
    }
    return x
}

fun main() {
    println(absInt(-7)) // 7
    println(absInt(7))  // 7
}

Щоб закріпити, корисно уявити «стрілочки» виконання. Функція — це коробка, return — вихід із коробки:

flowchart TD
    A["Виклик absInt(x)"] --> B{ x < 0 ? }
    B -->|так| C[return -x]
    B -->|ні| D[return x]

3. Ранній return: менше вкладеності, простіше читання

З досвідом приходить неприємне відкриття: що більше if усередині if усередині if, то сильніше хочеться закрити ноутбук і піти вирощувати помідори.

Ранній return (його ще називають guard clause, охоронною перевіркою) — це стиль, який робить код лінійним: спочатку відсікаємо невідповідні випадки й одразу виходимо, а потім пишемо «нормальний сценарій» без зайвих відступів.

Важливо: ранній return — це не «трюк», а звичайний інструмент, який робить код читабельнішим. Водночас ним легко зловживати, якщо перетворювати функцію на набір виходів «на кожному кроці».

Уявімо функцію, яка рахує ціну зі знижкою. Знижка має бути від 0..100, інакше можна ще й залишитися винними.

fun discountedPrice(price: Int, discountPercent: Int): Int {
    if (price < 0) return 0
    if (discountPercent < 0) return price
    if (discountPercent > 100) return 0

    val discount = price * discountPercent / 100
    return price - discount
}

fun main() {
    println(discountedPrice(1000, 25)) // 750
}

Зверніть увагу на стиль: «погані випадки — одразу виходимо», «хороший випадок — наприкінці». Так ми зменшуємо вкладеність і допомагаємо мозку рухатися кодом, не втрачаючи нитку.

І ще один важливий момент: return одразу завершує функцію. Це означає, що все нижче return у тій самій гілці ніколи не виконається. Іноді новачки очікують, що return — це «повернути значення, але продовжити далі», ніби це println. Ні: return — це саме вихід.

4. Unit: функції-дії без результату

Іноді функція нічого не повертає, а виконує дію: друкує текст, показує меню, записує лог, збільшує лічильник спроб. Для таких функцій у Kotlin є тип результату Unit. Він дуже схожий на void з інших мов, але технічно це справжній тип — просто з одним можливим значенням (самим Unit).

Зазвичай Unit у сигнатурі можна не писати, бо Kotlin і так розуміє: якщо ви не вказали тип результату, то це Unit. Іноді сигнатури з Unit можна побачити в прикладах і документації, наприклад: fun foo(): Unit — тип зазначено заради ясності та як демонстрацію.

Приклад функції-дії:

fun printHeader(title: String) {
    println("==== $title ====")
}

fun main() {
    printHeader("Калькулятор знижки")
    // ==== Калькулятор знижки ====
}

Тут немає чого повертати: виклик printHeader(...) — це просто крок сценарію.

Корисна таблиця, щоб не плутатися:

Що робить функція Тип результату Можна присвоїти в val x = Типовий приклад
Обчислює значення
Int, String, Boolean, ...
Так
val total = sum(1, 2)
Виконує дію
Unit
Формально можна, але майже ніколи не потрібно
printHeader("...")
Ніколи не повертається
Nothing
Ні (бо не повертає керування) нескінченний цикл

return у Unit-функціях

Якщо функція повертає Unit, це не означає, що return заборонено. Навпаки: return у Unit-функції — хороший спосіб зробити ранній вихід. Тільки виглядає він трохи інакше: без значення. Тобто просто return, без числа чи рядка.

Це зручно, коли в деяких ситуаціях ви хочете «нічого не робити», але водночас не хочете обгортати весь корисний код у величезний if.

Приклад: друкуємо повідомлення лише для додатних чисел.

fun printIfPositive(x: Int) {
    if (x <= 0) return
    println("Додатне: $x")
}

fun main() {
    printIfPositive(-3) // (нічого не друкує)
    printIfPositive(10) // Додатне: 10
}

Ще приклад: друкуємо меню, але якщо заголовок порожній — виходимо без друку, бо «меню без назви» виглядає сумно.

fun printMenu(title: String) {
    if (title.isBlank()) return

    println("== $title ==")
    println("1) Порахувати знижку")
    println("2) Вихід")
}

fun main() {
    printMenu("Меню")
    // == Меню ==
    // 1) Порахувати знижку
    // 2) Вихід
}

5. Nothing: функція, яка не повертає керування

Тип Nothing звучить як назва горор-фільму: «Ніщо: Повернення компілятора». На практиці це дуже цікава ідея: функція з типом результату Nothing ніколи не завершується нормально. Тобто вона не може повернути керування туди, звідки її викликали.

Вам поки що зарано заглиблюватися в теми, де це трапляється «по-справжньому». Тож найзрозуміліший приклад Nothing — нескінченний цикл.

Kotlin використовує Nothing і як частину системи типів: наприклад, в описах generics можна зустріти конструкції виду Function<in Nothing, out Any?> — тобто «нічого не можна безпечно передати», і це виражається саме Nothing. Вам поки достатньо запамʼятати головну ідею: Nothing — це «все, потяг далі не їде».

Найзрозуміліший приклад:

fun forever(): Nothing {
    while (true) {
        // Ми тут застрягли назавжди.
        // У реальній програмі так робити не треба.
    }
}

Чому це взагалі корисно, якщо «так робити не треба»? Тому що система типів Kotlin любить точність: якщо компілятор знає, що «після цієї точки виконання не продовжиться», він може краще аналізувати код і типи.

На вашому рівні зараз важливо запамʼятати просте формулювання: Nothing — це не «порожнє значення», а обіцянка «керування не повернеться».

6. Міні‑застосунок

Щоб тема не залишилася абстрактною, зберемо маленький консольний застосунок, який рахує ціну зі знижкою. Ми спеціально рознесемо логіку на функції, щоб побачити всі три варіанти: функцію‑обчислення (Int), функцію‑дію (Unit) і функцію‑приклад для Nothing (як демонстрацію ідеї, а не як частину нормального сценарію).

Функція‑обчислення: рахуємо знижку й повертаємо Int

Спочатку напишемо чисту обчислювальну функцію: вона нічого не друкує, не читає введення, а лише рахує й повертає результат. Таку функцію легше перевіряти «в голові» й повторно використовувати.

fun discountedPrice(price: Int, discountPercent: Int): Int {
    if (price < 0) return 0
    if (discountPercent < 0) return price
    if (discountPercent > 100) return 0

    val discount = price * discountPercent / 100
    return price - discount
}

fun main() {
    println(discountedPrice(1000, 25)) // 750
}

Тут ми вже використовуємо ранні return, і це не випадково: погані значення простіше відсікти одразу, ніж тягнути їх через усю логіку.

Функція‑дія: друкуємо результат

Тепер додамо функцію, яка лише друкує. Вона нічого не повертає назовні — її сенс у тому, щоб виконати дію.

fun printResult(original: Int, discount: Int, result: Int) {
    println("Ціна: $original")
    println("Знижка: $discount%")
    println("Разом: $result")
}

fun main() {
    val result = discountedPrice(1000, 25)
    printResult(1000, 25, result)
    // Ціна: 1000
    // Знижка: 25%
    // Разом: 750
}

Зверніть увагу: printResult не повертає значення, але це не робить її «дивною». У програмах половина функцій — саме такі: «зробити», а не «порахувати».

return без значення: мʼяко виходимо з друку, якщо введення дивне

Іноді дію краще не виконувати, якщо дані не мають сенсу. Припустімо, ми не хочемо друкувати результат, якщо ціна дорівнює нулю (для демонстрації — бо «знижки на повітря» звучить філософськи).

fun printResultIfMeaningful(original: Int, discount: Int, result: Int) {
    if (original == 0) return
    println("Разом після знижки: $result")
}

fun main() {
    printResultIfMeaningful(0, 25, 0)      // (нічого)
    printResultIfMeaningful(1000, 25, 750) // Разом після знижки: 750
}

Ось він, return без значення — цілком нормальний інструмент для Unit-функцій.

Збираємо main: введення, розрахунок, друк

Зараз у нас достатньо деталей, щоб зібрати простий сценарій. Поки що обійдемося без «розумного» введення та без оброблення помилок через винятки — це буде пізніше. Тут важливо побачити, як повернені значення «течуть» із функції у функцію.

fun main() {
    println("Введіть ціну:")
    val price = readln().toInt()

    println("Введіть знижку (0..100):")
    val discount = readln().toInt()

    val result = discountedPrice(price, discount)
    printResult(price, discount, result)
}

Зверніть увагу на композицію: discountedPrice(...) повертає Int, ми зберігаємо його в result, а потім передаємо в printResult(...), яка повертає Unit і просто друкує результат.

Демонстрація Nothing: «функція, яка не відпускає»

І нарешті — маленька демонстрація Nothing. У нормальному застосунку так робити не потрібно, але для розуміння типу корисно хоча б раз це побачити.

fun freeze(): Nothing {
    while (true) {
        println("Я застряг у циклі й не повернуся назад…")
    }
}

fun main() {
    println("Перед freeze")
    freeze()
    println("Після freeze") // цей рядок ніколи не виконається
}

Ви ніколи не побачите в консолі «Після freeze». І це саме те, що означає Nothing: керування не повертається.

7. Типові помилки

Помилка №1: не всі гілки повертають значення у функції з типом результату.
Найчастіший випадок — if з return усередині, а далі — «якось воно буде». Компілятор не телепат і не зобовʼязаний здогадуватися. Лікується просто: або додаєте return після if, або робите повноцінний if/else, де обидві гілки повертають потрібний тип.

Помилка №2: спроба «повернути значення» з Unit-функції.
Новачки іноді пишуть return 5 усередині функції, яка оголошена як fun f() { ... }. Компілятор сваритиметься — і матиме рацію: або функція обчислює й повинна мати : Int, або це дія, і тоді вона повертає Unit.

Помилка №3: очікування, що return працює як break або «просто завершує if».
return завершує саме функцію, а не блок if і не цикл. Якщо всередині циклу ви написали return, то вийшли з функції повністю. Для циклів у нас є інші інструменти (break/continue).

Помилка №4: використання Nothing як «порожнього результату».
Nothing — не «порожнє значення» і не «нічого не повернути». Це саме «не повернутися взагалі». Якщо ви бачите Nothing, думайте про нескінченний цикл (у межах сьогоднішньої лекції) і про те, що код після виклику такої функції не виконається.

Помилка №5: друк усередині обчислювальної функції без ясної причини.
Коли функція і рахує, і друкує, її складніше повторно використовувати: ви хотіли просто отримати число, а вона вже щось вивела в консоль. Іноді так можна (особливо в маленьких скриптах), але частіше краще розділяти: одна функція повертає значення, інша — друкує (наприклад, через println). Це робить програму передбачуванішою й простішою для налагодження.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ