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 + явный тип | |
Когда хотите, чтобы контракт читался сразу |
| Expression body + вывод типа | |
Когда выражение максимально очевидное |
| Блочное тело | |
Когда несколько шагов, 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 что-то меняет, это должно читаться из кода, а не быть сюрпризом.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ