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. Нет, это именно выход.

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("Positive: $x")
}

fun main() {
    printIfPositive(-3) // (ничего не печатает)
    printIfPositive(10) // Positive: 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). Это делает программу предсказуемее и проще для отладки.

1
Задача
Kotlin SELF, 8 уровень, 2 лекция
Недоступна
Датчик вибраций
Датчик вибраций
1
Задача
Kotlin SELF, 8 уровень, 2 лекция
Недоступна
Индикатор направления
Индикатор направления
1
Задача
Kotlin SELF, 8 уровень, 2 лекция
Недоступна
Деление без аварии
Деление без аварии
1
Задача
Kotlin SELF, 8 уровень, 2 лекция
Недоступна
Плакат события
Плакат события
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ