JavaRush /Курсы /Kotlin SELF /Лямбда‑выражения в Kotlin: синтаксис, it, возвращаемое зн...

Лямбда‑выражения в Kotlin: синтаксис, it, возвращаемое значение

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

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 -> x + 1 }
x слева от -> параметр лямбды
{ x -> ... }
->
«от параметров к телу»
{ x -> x + 1 }
выражения справа тело лямбды
{ x -> x + 1 }

И вот пример чуть длиннее, но всё ещё простой:

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, либо тип переменной слева. Это нормально: компилятор не придирается, он просто требует однозначности, чтобы потом не гадать, какую версию кода запускать.

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