1. Знакомство с лямбда-функциями
Если вы пока слабо разбираетесь в программировании, лямбды могут звучать как «какая-то магия функциональщиков». На самом деле идея очень житейская: иногда нам хочется обращаться с действием так же, как с числом или строкой. Число можно положить в переменную, передать в функцию, вернуть из функции. Так вот, с лямбдой мы делаем то же самое — только храним не данные, а маленькую логику.
Представьте, что в нашем консольном учёте расходов мы в одном месте хотим считать «крупным расходом» сумму от 1000, а в другом — от 500. Если правило зашито в if, придётся копировать if (amount >= 1000) по всему проекту и потом героически искать, где забыли обновить. Лямбда позволяет написать «правило» один раз, присвоить его переменной и использовать в нужных местах.
2. Лямбда как значение: сохраняем в val и вызываем
Самое важное, что нужно почувствовать руками: лямбда — не «особый оператор», а значение. То есть её реально можно сохранить в переменную. А раз это значение, то его можно потом «вызвать», как обычную функцию.
Начнём с максимально простого примера: проверка чётности числа.
fun main() {
val isEven: (Int) -> Boolean = { x -> x % 2 == 0 }
println(isEven(4)) // true
println(isEven(5)) // false
}
Здесь происходит сразу несколько полезных вещей.
Во-первых, справа от = мы создали лямбду { x -> x % 2 == 0 }.
Во-вторых, слева мы честно указали тип переменной: (Int) -> Boolean. Пока не углубляйтесь — считайте, что это «тип функции». Он буквально говорит: «принимает Int, возвращает Boolean».
В-третьих, внизу мы вызываем лямбду так же, как функцию: isEven(4). То есть переменная isEven ведёт себя как функция.
Чтобы закрепить идею «лямбда — значение», вот ещё пример, где лямбда возвращает строку:
fun main() {
val greet: (String) -> String = { name -> "Привет, $name!" }
println(greet("Котлин")) // Привет, Котлин!
}
Смысл тот же: мы храним «формулу» приветствия в переменной, и можем вызывать её сколько угодно раз.
Создать и вызвать — разные действия
Даже в этой первой лекции полезно выработать одну привычку: отличать место, где лямбду создают, от места, где её вызывают.
Сравните:
fun main() {
val inc: (Int) -> Int = { it + 1 } // создаём и сохраняем
val a = inc(10) // вызываем
println(a) // 11
}
Если вы написали { it + 1 }, но нигде не написали inc(10), то ничего не произойдёт. Лямбда не «работает сама по себе», она не фоновый демон и не таймер.
3. Синтаксис лямбды и параметр it
Базовая форма: { параметры -> тело }
В лямбдах легко запутаться, потому что там фигурные скобки, стрелка и ощущение, что вы случайно открыли портал в другое измерение. Давайте приручим это как обычную грамматику языка: «вот где параметры, вот где тело, вот что возвращается».
Базовая форма лямбды в Kotlin выглядит так: всегда фигурные скобки, внутри параметры, потом стрелка ->, потом тело. Параметры (с необязательными типами) идут до ->, тело после, а возвращаемое значение берётся из последнего выражения.
Вот мини-табличка «как читать»:
| Фрагмент | Что это значит | Пример |
|---|---|---|
|
«Это лямбда» (всегда в фигурных скобках) | |
| x слева от -> | параметр лямбды | |
|
«от параметров к телу» | |
| выражения справа | тело лямбды | |
И вот пример чуть длиннее, но всё ещё простой:
fun main() {
val addVat: (Int) -> Int = { price ->
price + (price * 20 / 100)
}
println(addVat(100)) // 120
println(addVat(250)) // 300
}
Здесь параметр называется price, тело — это выражение price + ..., результатом лямбды будет итог последнего выражения.
Обратите внимание на важную привычку чтения: когда видите { ... -> ... }, мысленно заменяйте это на «маленькая функция без имени». Не на «волшебный блок», не на «особый синтаксис списков», а именно на функцию.
Один параметр — можно использовать it
Когда у лямбды ровно один параметр, Kotlin часто позволяет его не объявлять явно. Тогда этот параметр автоматически доступен под именем it. Если сигнатура понятна из контекста и параметр один, имя можно опустить, и компилятор заведёт it.
Пример: проверка «положительное ли число»:
fun main() {
val isPositive: (Int) -> Boolean = { it > 0 }
println(isPositive(10)) // true
println(isPositive(-1)) // false
}
Здесь it — это тот самый единственный Int, который приходит в лямбду.
Важно не превратить it в религию. it хорош, когда условие короткое и очевидное. Если логика сложнее, лучше назвать параметр по смыслу, иначе вы будете читать код как загадку: «а it — это что… число? строка? кот?».
Сравните два варианта:
fun main() {
val ok1: (String) -> Boolean = { it.trim().isNotEmpty() }
val ok2: (String) -> Boolean = { text -> text.trim().isNotEmpty() }
println(ok1(" ")) // false
println(ok2("hi")) // true
}
Оба варианта корректны. Второй иногда читается спокойнее, особенно если вы потом добавите ещё пару строк в тело лямбды.
4. Возвращаемое значение и многострочные лямбды
Что возвращает лямбда: результат последнего выражения
С лямбдами в Kotlin есть очень удобная договорённость: если вы явно не пишете return@..., то значением лямбды считается последнее выражение в её теле.
Посмотрим на примере. Мы хотим получить строковое описание числа:
fun main() {
val describe: (Int) -> String = { n ->
val sign = if (n >= 0) "non-negative" else "negative"
"n=$n ($sign)"
}
println(describe(7)) // n=7 (non-negative)
println(describe(-3)) // n=-3 (negative)
}
Последняя строка внутри лямбды — это строка "n=$n ($sign)". Значит именно она и станет результатом лямбды.
Очень полезная бытовая проверка: «чем заканчивается лямбда?» Если она заканчивается println(...), то результатом будет Unit (потому что println возвращает Unit). Если заканчивается выражением, которое вычисляется в значение, значит и лямбда вернёт это значение.
Вот пример «лямбда, которая делает действие» (и возвращает Unit):
fun main() {
val log: (String) -> Unit = { msg ->
println("LOG: $msg")
}
log("Стартуем!") // LOG: Стартуем!
}
В учебных задачах часто встречается путаница: «я думал, что раз лямбда написана, она уже что-то сделает». Нет — пока вы её не вызвали, она просто лежит в переменной, как рецепт борща. Рецепт сам борщ не готовит.
Многострочная лямбда: как маленькая функция
Когда вы впервые видите лямбду на 5–6 строк, мозг может начать паниковать: «почему функция без имени живёт в фигурных скобках, это вообще законно?». Законно. И даже полезно — если делать это аккуратно.
Многострочная лямбда читается так же, как обычная функция: внутри можно объявлять промежуточные val, можно делать if как выражение, а в конце вернуть итог.
Например, сделаем лямбду, которая форматирует сумму денег для вывода (условно: большие суммы помечаем "(!)"):
fun main() {
val formatAmount: (Int) -> String = { amount ->
val badge = if (amount >= 1000) " (!)" else ""
"$amount$badge"
}
println(formatAmount(120)) // 120
println(formatAmount(1500)) // 1500 (!)
}
Обратите внимание на стиль: мы не пытаемся впихнуть всё в одну строку, но и не делаем из лямбды «роман в трёх томах». Внутри есть один промежуточный val, а последняя строка — финальный результат.
Если хочется ещё чуть «приблизить к реальности», возьмём элемент нашего практического приложения. Мы пока храним расход как Triple(id, category, amount) (без классов — потому что до ООП мы дойдём позже).
fun main() {
val formatExpense: (Triple<Int, String, Int>) -> String = { exp ->
val (id, category, amount) = exp
val mark = if (amount >= 1000) " (!)" else ""
"#$id $category: $amount$mark"
}
println(formatExpense(Triple(1, "food", 450))) // #1 food: 450
println(formatExpense(Triple(2, "rent", 15000))) // #2 rent: 15000 (!)
}
Технически это всё ещё «просто лямбда», но по ощущению — маленькая функция форматирования, которую можно переиспользовать в разных местах, не копируя строковые шаблоны.
5. Типы в лямбдах: где Kotlin догадается сам
До этого момента мы честно писали тип переменной слева: (Int) -> Boolean, (String) -> String и так далее. Но Kotlin часто умеет выводить тип сам — либо из типов параметров, либо из контекста.
Простейший пример: тип параметра указан прямо в лямбде, значит тип всей лямбды понятен:
fun main() {
val square = { x: Int -> x * x }
println(square(5)) // 25
}
Здесь тип переменной square выводится автоматически: раз параметр Int, и результат Int, значит это что-то вроде «функция Int -> Int».
А вот пример, где Kotlin не может догадаться, потому что вы написали слишком «абстрактно»:
fun main() {
// Так делать не стоит: компилятору неоткуда узнать тип x
// val id = { x -> x }
val idInt: (Int) -> Int = { x -> x }
println(idInt(42)) // 42
}
Почему первая версия плохая? Потому что x без типа, контекста нет, и компилятор не умеет читать мысли. В таких случаях вы либо указываете тип параметра прямо в лямбде ({ x: Int -> x }), либо указываете тип переменной слева (val idInt: (Int) -> Int = ...).
6. Мини‑пример: правило и форматирование как лямбды
Давайте сделаем маленький, но практичный шаг в сторону «живого» кода. Мы не будем пока строить сложные абстракции — просто покажем, что логику можно положить в переменные: отдельно «как печатать», отдельно «что считать крупным».
Представим, что в каком-то месте нашего приложения мы хотим вывести список расходов, но помечать крупные суммы. Сделаем две лямбды: isBig и format.
fun main() {
val expenses = listOf(
Triple(1, "food", 450),
Triple(2, "transport", 120),
Triple(3, "rent", 15000)
)
val isBig: (Int) -> Boolean = { it >= 1000 }
val format: (Triple<Int, String, Int>) -> String = { exp ->
val (id, category, amount) = exp
val mark = if (isBig(amount)) " (!) " else " "
"#$id$mark$category: $amount"
}
for (e in expenses) {
println(format(e))
// #1 food: 450
// #2 transport: 120
// #3 (!) rent: 15000
}
}
Здесь приятно то, что если завтра вы решите, что «крупный» — это от 5000, вы поменяете одно место: val isBig = { it >= 5000 }. И форматирование автоматически начнёт использовать новое правило — потому что format внутри вызывает isBig(amount).
Это ещё не «передача поведения в функции» (до неё мы доберёмся чуть позже), но уже хороший переход от «всё зашито в if» к более аккуратной структуре.
7. Типичные ошибки при работе с лямбда‑выражениями
Ошибка №1: использовать it, когда параметров больше одного.
it существует только для лямбды с одним параметром. Если параметров два, придётся писать { a, b -> ... }. Если попытаться «протащить» it, код либо не скомпилируется, либо станет нечитабельным, потому что вы начнёте изобретать странные имена и путаться, что куда пришло.
Ошибка №2: ждать, что лямбда выполнится сама, раз она написана.
Лямбда — значение. Она не запускается в момент объявления. Пока вы не вызвали её через f(x) (или не передали куда-то, где её вызовут), она просто лежит в переменной. Очень типичный симптом: «я написал { println("hi") }, а оно ничего не печатает».
Ошибка №3: забыть, что возвращается последнее выражение, и случайно вернуть Unit.
Когда в конце лямбды стоит println(...), лямбда фактически возвращает Unit. Иногда это неожиданно, потому что вы «вроде бы посчитали значение», но последней строкой сделали печать. Лечится просто: делайте вычисление последним выражением (а печать — отдельным шагом), или сохраняйте вычисление в val и возвращайте его последней строкой. Правило «последнее выражение становится результатом» — базовое.
Ошибка №4: писать лямбду слишком большой прямо на месте и терять смысл.
Если лямбда стала на 15 строк, в ней три if, два счётчика и немного философии — мозгу тяжело. Даже если формально это работает, читать такой код неприятно. В таких случаях обычно лучше либо дать параметру смысловое имя (вместо вечного it), либо вынести логику в отдельную именованную функцию, а лямбду оставить короткой и ясной.
Ошибка №5: оставить типы полностью без подсказок и получить Cannot infer a type for this parameter.
Лямбда вроде { x -> x } выглядит логично по-человечески, но компилятор не обязан понимать, что вы имели в виду. Если контекст не подсказывает тип, Kotlin попросит вас уточнить: либо x: Int, либо тип переменной слева. Это нормально: компилятор не придирается, он просто требует однозначности, чтобы потом не гадать, какую версию кода запускать.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ