JavaRush /Курсы /Kotlin SELF /Expression body и локальные функции

Expression body и локальные функции

Kotlin SELF
8 уровень , 4 лекция
Открыта

1. Функция как выражение

Когда вы начинаете активно выносить код в функции, случается типичная история: файл становится приятнее, main() — короче, но… функций появляется много. И среди них встречаются такие, которые по смыслу логичные, а по размеру выглядят как «5 строк ради одной мысли». Слишком много мелких функций может быть неудобно.

С другой стороны, если всё оставить в одном main, получится «простыня». Поэтому нам нужен баланс: маленькие функции — хорошо, но и код ради кода иногда мешает. В Kotlin у нас два инструмента, чтобы держать этот баланс: expression body (короткое тело функции через =) и локальные функции (функции внутри других функций).

Expression body

Expression body — это способ сказать Kotlin: «у этой функции тело состоит из одного выражения, давай запишем её красиво, без фигурных скобок и return». Допустим, нам нужна функция для возведения в квадрат. Вот её классическая запись:

fun square(x: Int): Int {
    return x * x
}

Но мы можем написать её и покороче:

fun square(x: Int): Int = x * x

Смысл тот же, но запись ближе к математической формуле. Для простых вычислений и проверок это часто читается лучше: функция выглядит как определение, а не как мини‑сценарий.

Мини‑пример 1: вычисление x в кубе

fun cube(x: Int): Int = x * x * x

fun main() {
    println(cube(3)) // 27
}

Мини‑пример 2: проверка на чётность

fun isEven(x: Int): Boolean = x % 2 == 0

fun main() {
    println(isEven(10)) // true
    println(isEven(7))  // false
}

Здесь важно почувствовать суть: isEven читается как вопрос, а справа — «ответ‑формула». Для начинающих это, честно говоря, один из самых приятных способов читать код.

2. Expression body: вывод типа и когда лучше блочное тело

У expression body есть приятный бонус: Kotlin часто сам понимает возвращаемый тип по выражению справа. То есть можно писать ещё короче:

fun square(x: Int) = x * x

И компилятор выведет тип как Int.

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

Сравним в таблице:

Вариант Пример Когда использовать
Expression body + явный тип
fun isEven(x: Int): Boolean = x % 2 == 0
Когда хотите, чтобы контракт читался сразу
Expression body + вывод типа
fun isEven(x: Int) = x % 2 == 0
Когда выражение максимально очевидное
Блочное тело
fun f(...) { ... }
Когда несколько шагов, if, цикл, переменные

Мини‑пример: строка как результат

fun formatMoney(amount: Int): String = "$amount €"

fun main() {
    println(formatMoney(120)) // 120 €
}

Когда expression body лучше не использовать

У expression body есть ловушка: раз можно короче, надо короче. В результате код превращается в однострочные монстры, где половина логики прячется в скобках, и читать это хочется только под угрозой дедлайна.

Если внутри функции появляется несколько шагов, промежуточные переменные, цикл или разветвление, то блочное тело обычно логичнее: оно показывает, что тут не формула, а процесс.

Сравните ощущение. Так — нормально и читаемо:

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

Так — уже спорно для новичка (хотя технически корректно):

fun absInt(x: Int): Int = if (x < 0) -x else x

4. Мини‑приложение: «Копилка расходов»

Чтобы не обсуждать компактность в вакууме, давайте продолжим развивать одно консольное приложение. Пусть это будет простая «копилка расходов»: пользователь вводит несколько расходов (суммы), а программа считает итог, максимум и среднее.

Сначала набросаем простой каркас (как будто он уже был в прошлых лекциях), а потом начнём «причёсывать» его.

fun main() {
    val amounts = intArrayOf(120, 80, 200, 50)

    val total = sumAmounts(amounts)
    val max = maxAmount(amounts)
    val avg = averageAmount(amounts)

    println("Итого: $total")     // Итого: 450
    println("Максимум: $max")    // Максимум: 200
    println("Среднее: $avg")     // Среднее: 112.5
}

Функции пока сделаем обычными, чтобы было с чем работать:

fun sumAmounts(xs: IntArray): Int {
    var total = 0
    for (x in xs) {
        total += x
    }
    return total
}

fun maxAmount(xs: IntArray): Int {
    var m = xs[0]
    for (x in xs) {
        if (x > m) m = x
    }
    return m
}

fun averageAmount(xs: IntArray): Double {
    val total = sumAmounts(xs)
    return total.toDouble() / xs.size
}

Теперь смотрим глазами «редактора кода»: какие функции здесь кандидаты на expression body? averageAmount — да, потому что в ней логика почти формульная (хотя есть промежуточная переменная). Можно сделать ещё чуть компактнее, но без фанатизма.

Вариант аккуратный, всё ещё читаемый:

fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size

И вот тут хорошо видно главное достоинство: мы не «сжали код ради сжатия», а сделали функцию похожей на определение среднего.

Давайте обновим пример целиком (обратите внимание: мы оставили sumAmounts и maxAmount блочными, потому что там цикл — это уже не «одно выражение»):

fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size

fun main() {
    val amounts = intArrayOf(120, 80, 200, 50)
    println(averageAmount(amounts)) // 112.5
}

5. Локальные функции в приложении

Иногда функция нужна только как маленький помощник внутри одного сценария. Выносить её на уровень файла — значит «засорять пространство имён»: появляется ещё одно имя, ещё одна штука, которую можно случайно использовать не там, где надо. Локальная функция решает это: вы объявляете fun helper(...) прямо внутри другой функции, и она существует только там.

Это похоже на то, как вы заводите локальную переменную: вы же не объявляете каждую временную переменную глобально на весь файл. Вот и с локальными функциями такая же идея: «живёт рядом с местом использования и не мешает остальным».

Важный момент: локальная функция может обращаться к переменным внешней функции и даже менять их (то есть использовать «внешнее окружение»). Это поведение такое же, как у других вложенных конструкций, которые могут захватывать переменные из внешнего scope.

Ввод с парсингом и валидацией

Давайте сделаем следующий шаг в «копилке расходов»: вместо жёстко заданного массива будем читать расходы с консоли. Но у нас сразу появляется типичный сценарий ввода: прочитать строку, почистить пробелы, распарсить число, проверить, что оно положительное. И этот сценарий часто получается либо дублирующим, либо слишком длинным внутри main.

Сделаем функцию readPositiveInt(prompt: String): Int, а внутри неё спрячем локальные помощники.

fun readPositiveInt(prompt: String): Int {
    fun parseIntOrNull(text: String): Int? {
        val cleaned = text.trim()
        return cleaned.toIntOrNull()
    }

    while (true) {
        print(prompt)
        val raw = readln()
        val value = parseIntOrNull(raw)

        if (value != null && value > 0) return value
        println("Введите целое число больше 0.")
    }
}

Здесь локальная функция parseIntOrNull не нужна всей программе — она нужна только этому вводу. Поэтому держать её внутри — логично: читаешь readPositiveInt, и сразу видишь, как именно мы парсим строку.

Небольшая блок‑схема процесса ввода

flowchart TD
    A[Показать prompt] --> B[Прочитать строку]
    B --> C[trim]
    C --> D[toIntOrNull]
    D --> E{value != null и value > 0?}
    E -- да --> F[return value]
    E -- нет --> G[Сообщение об ошибке]
    G --> A

Теперь используем эту функцию в main и соберём массив расходов фиксированного размера. Размер тоже попросим у пользователя — так будет похоже на реальную программу.

fun main() {
    val n = readPositiveInt("Сколько расходов ввести? ")

    val amounts = IntArray(n)
    for (i in 0 until n) {
        amounts[i] = readPositiveInt("Расход #${i + 1}: ")
    }

    println("Итого: ${sumAmounts(amounts)}") // например: Итого: 450
}

Комбинируем подходы: helpers и expression body

Самая приятная часть начинается, когда вы комбинируете инструменты. Локальные функции помогают спрятать одноразовые детали, а expression body делает «формульные» функции короткими и читаемыми.

Например, в нашем приложении можно сделать маленькую функцию форматирования суммы, и она идеально подходит под expression body. Причём это полезно даже сейчас, когда кажется «да я и так могу написать $amount € прямо в println». Можете — но потом вы захотите поменять формат (например, добавить слово «евро»), и будете искать по файлу все места, где печатали сумму.

fun formatMoneyEuro(amount: Int): String = "$amount €"

А внутри main:

fun main() {
    val amounts = intArrayOf(120, 80, 200, 50)
    val total = sumAmounts(amounts)

    println("Итого: ${formatMoneyEuro(total)}") // Итого: 450 €
}

Ещё один хороший кандидат — «среднее», мы уже делали:

fun averageAmount(xs: IntArray): Double = sumAmounts(xs).toDouble() / xs.size

Теперь чуть улучшим итоговый вывод, но аккуратно: без огромных форматирований и без новых тем.

fun printReport(amounts: IntArray) {
    val total = sumAmounts(amounts)
    val max = maxAmount(amounts)
    val avg = averageAmount(amounts)

    println("Итого: ${formatMoneyEuro(total)}")     // Итого: 450 €
    println("Максимум: ${formatMoneyEuro(max)}")    // Максимум: 200 €
    println("Среднее: $avg")                       // Среднее: 112.5
}

И main становится «сценарием», а не «мешком логики»:

fun main() {
    val n = readPositiveInt("Сколько расходов ввести? ")

    val amounts = IntArray(n)
    for (i in 0 until n) {
        amounts[i] = readPositiveInt("Расход #${i + 1}: ")
    }

    printReport(amounts)
}

Вот здесь как раз видно, зачем всё это: main читается как последовательность шагов, а детали разложены по функциям. При этом мы не плодим лишние «одноразовые» функции на уровне файла: то, что нужно только внутри ввода, живёт локально.

6. Типичные ошибки при expression body и локальных функциях

Ошибка №1: превращать expression body в «компрессор мозга».
Иногда хочется упаковать в = всё подряд, включая сложные условия и многоэтажные вычисления. Формально компилятор это переварит, но человек — не всегда. Если строка перестаёт читаться «с первого взгляда», лучше вернуться к блочному телу и честно расписать шаги.

Ошибка №2: пытаться сделать expression body там, где нужен цикл или несколько действий.
Expression body — это про одно выражение. Если у вас появился for, накопление суммы, обновление переменной, печать промежуточных значений — это уже сценарий, и ему подходит обычное { ... }. Не воспринимайте блочное тело как «хуже»: оно просто для другого типа задач.

Ошибка №3: выносить наружу функции, которые нужны ровно в одном месте.
Часто новичок делает десяток маленьких helper‑функций на уровне файла, а потом сам же путается, какая где используется. Локальная функция — хороший компромисс: она даёт имя шагу, но не раздувает внешний API файла. Если помощник используется только внутри одной функции ввода — пусть он и живёт там.

Ошибка №4: переусложнять локальные функции и прятать важную логику слишком глубоко.
Локальная функция должна быть маленькой и «по делу». Если вы внутри readPositiveInt объявили 5 локальных функций, и каждая по 20 строк — это уже не «спрятали детали», а «сделали квест». В таком случае лучше вынести часть на уровень файла и дать ей нормальное имя.

Ошибка №5: случайно полагаться на внешние переменные и получать неожиданные эффекты.
Локальные функции могут обращаться к переменным внешней функции и менять их — это удобно, но может стать ловушкой, если вы не заметили, что helper изменяет состояние. Держите такие изменения очевидными: если helper что-то меняет, это должно читаться из кода, а не быть сюрпризом.

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