Функции и лямбды
Зачем это нужно
В прошлой главе мы уже пользовались готовыми функциями — println, listOf, map, sum — и даже написали свою первую лямбду внутри map { it * 2 }. Мы знали, как их вызывать, но не разбирали, как устроена функция изнутри и как написать свою.
Функция (function) — это именованный кусок кода, который можно вызвать сколько угодно раз с разными входными данными. Без функций код, который считает бонус за очки, пришлось бы копировать в каждое место, где он нужен: на экране игрока, в отчёте тренера, в уведомлении о победе. С функцией это один кусок кода в одном месте — и вызов по имени везде, где он нужен.
В этой главе разберём объявление функции по символам — что значит каждый знак сигнатуры, — научимся задавать параметры по умолчанию (default arguments) и именованные аргументы (named arguments), напишем собственные лямбды разных форм и разберёмся, что такое функция высшего порядка (higher-order function) на примере map и filter, которые уже видели в главе 1.
- Зачем это нужно
- Функция по кусочкам:
fun square(x: Int): Int - Тело-выражение: одна строка вместо блока
- Параметры по умолчанию: необязательные аргументы
- Именованные аргументы: вызов по имени, а не по порядку
- Лямбды: с
itи с именованными параметрами - Функции высшего порядка:
map,filterи свои - Типичные ошибки
- Символы главы
- Слова главы
- Потренируйся печатать
- Куда дальше: проверенные ресурсы
- Челлендж ⭐
- Что должен уметь
Функция по кусочкам: fun square(x: Int): Int
Вот функция, которая возводит число в квадрат. Разберём сигнатуру — первую строку, где объявлены имя, параметры и тип возврата, — по символам.
Что означает каждый символ
fun— ключевое слово: объявляет функцию. Как иval/var, это слово нельзя использовать как имя переменной или функции.- скобки
( )— список параметров (parameters): переменных, которые функция получает при вызове. Пустые скобки()означают, что функция параметров не принимает — так была устроенаmain()в главе 1. : Типпосле скобок — тип возврата (return type): что функция отдаёт наружу. Если функция ничего не возвращает — этот раздел можно опустить, разберём чуть ниже.- параметр и аргумент — разные слова. Параметр (parameter) — это имя в объявлении функции (
xвfun square(x: Int)). Аргумент (argument) — конкретное значение, которое передают при вызове (4вsquare(4)). Одно описывает место, другое — то, что в это место кладут.
Теперь тело функции — то, что происходит внутри фигурных скобок, — и вызов.
fun main() {
println(square(4))
}
fun square(x: Int): Int {
return x * x
}
Разбор по строкам
- Строки 1–3:
fun main() { ... }— точка входа, как в главе 1. Внутри — единственный вызовsquare(4), обёрнутый вprintln. - Строка 5:
fun square(x: Int): Int {— объявление функцииsquare: принимает один параметрxтипаInt, возвращаетInt. Открывающая{начинает тело — список команд, которые выполнятся при вызове. - Строка 6:
return x * x— новое ключевое словоreturn: «немедленно закончи функцию и отдай наружу то, что справа». Здесь отдаётсяx * x— квадрат переданного числа. - Строка 7:
}— конец тела функции.
Функцию можно объявить и до, и после места, где её вызывают: в отличие от переменных, порядок объявления функций в файле не важен — компилятор сначала видит все объявления, а потом проверяет тела.
Что выведет: 16 — потому что square(4) вычислил 4 * 4 и вернул это значение в println.
Тело-выражение: одна строка вместо блока
Если тело функции — это ровно одно выражение, фигурные скобки и return можно убрать, заменив их знаком =. Это называется телом-выражением (expression body) — в противовес телу-блоку (block body), которое мы только что видели.
fun main() {
println(square(4))
}
fun square(x: Int): Int = x * x
Что выведет: 16 — то же самое число, что и в блочной версии: это два разных способа написать одну и ту же функцию.
У тела-выражения есть бонус: раз результат — это значение стоящего справа выражения, Kotlin может вывести его тип сам, точно так же, как выводил тип переменной по значению справа от =. Поэтому : Int можно вообще убрать.
fun main() {
println(square(4))
}
fun square(x: Int) = x * x
Что выведет: 16 — компилятор сам понял, что x * x — это Int, раз x типа Int, и присвоил такой же тип возврата функции. Это тот же вывод типов (type inference), что и у переменных, только применённый к функции.
Важно: этот трюк работает только для тела-выражения. У тела-блока (в фигурных скобках) тип возврата выводить неоткуда — Kotlin не станет анализировать все return внутри блока в поисках общего типа. Про это — отдельная ошибка в разделе «Типичные ошибки» ниже.
А что, если функции нечего возвращать — она просто что-то делает, например печатает текст? Тогда тип возврата вообще не пишут, и Kotlin считает его равным Unit — специальному типу «ничего не возвращаю».
fun main() {
greet("Соня")
}
fun greet(name: String) {
println("Привет, $name!")
}
Разбор по строкам
- Строка 5:
fun greet(name: String) {— после скобок нет: Тип— значит, тип возврата не указан, и Kotlin подставляетUnitсам. Писать: Unitруками можно, но так не делают — это единственный тип, который по соглашению не пишут явно. - Строка 6 — функция просто печатает строку и ничего не отдаёт наружу через
return.
Что выведет: Привет, Соня! — функция выполнила действие, но значения не вернула.
Параметры по умолчанию: необязательные аргументы
Параметру можно задать значение по умолчанию (default argument) прямо в объявлении. Тогда при вызове его можно не передавать — Kotlin подставит значение сам.
fun main() {
greet("Олег")
greet("Олег", "Здравствуй")
}
fun greet(name: String, greeting: String = "Привет") {
println("$greeting, $name!")
}
Разбор по строкам
- Строка 2:
greet("Олег")— передан толькоname;greetingне указан, поэтому берётся значение по умолчанию"Привет". - Строка 3:
greet("Олег", "Здравствуй")— передано два аргумента, второй перекрывает значение по умолчанию. - Строка 6 — само объявление:
greeting: String = "Привет"делает второй параметр необязательным.
Что выведет:
Привет, Олег!
Здравствуй, Олег!
Первый вызов взял значение по умолчанию, второй — то, что передали явно.
Обязательные параметры (без =) по соглашению ставят первыми, необязательные (со значением по умолчанию) — после них: это стиль, а не запрет компилятора. У параметра, который не в конце списка, тоже может быть значение по умолчанию — но тогда, чтобы пропустить его и задать следующий, понадобятся именованные аргументы, о которых дальше.
Именованные аргументы: вызов по имени, а не по порядку
Аргументы можно передавать не только по порядку, но и по имени параметра — тогда порядок вызова не важен, а из кода сразу видно, что чему соответствует.
fun main() {
describe(name = "ПГК", score = 100, isChampion = true)
describe(score = 42, name = "Соня")
}
fun describe(name: String, score: Int, isChampion: Boolean = false) {
println("$name: $score очков, чемпион: $isChampion")
}
Разбор по строкам
- Строка 2 — все три аргумента именованные, порядок совпадает с объявлением.
- Строка 3 — аргументы переставлены местами (
scoreраньшеname) и один пропущен:isChampionне указан, значит берётся значение по умолчаниюfalse. Именованные аргументы можно расставлять в любом порядке — Kotlin смотрит на имя слева от=, а не на позицию. - Строка 7 — обычное объявление функции с тремя параметрами, третий — со значением по умолчанию.
Что выведет:
ПГК: 100 очков, чемпион: true
Соня: 42 очков, чемпион: false
Заметь: name = "ПГК" при вызове и greeting: String = "Привет" при объявлении выглядят похоже, но это разные вещи. В объявлении имя: Тип = значение задаёт значение по умолчанию. При вызове имя = значение — это именованный аргумент, у него нет двоеточия и типа, потому что тип параметр уже получил при объявлении.
Проверь себя: здесь встречаются и значение по умолчанию, и именованные аргументы из этого раздела.
fun main() {
greet("Соня")
greet(greeting = "Здравствуй", name = "Олег")
}
fun greet(name: String, greeting: String = "Привет") {
println("$greeting, $name!")
}Что напечатает этот код? Сначала ответь, потом проверяй.
Лямбды: с it и с именованными параметрами
В главе 1 мы уже писали numbers.map { it * 2 } — лямбду (lambda) с неявным параметром it. Освежим и пойдём глубже: что делать, если параметров несколько или it неудобно читать.
fun main() {
val distances = listOf(3, 5, 4, 6, 2)
val long = distances.filter { it > 4 }
println(long)
}
Разбор по строкам
- Строка 3 — новый метод
filter: пропускает каждый элемент через лямбду и оставляет в результате только те, для которых лямбда вернулаtrue.itздесь — очередной элемент списка,it > 4— сравнение «больше 4». - Что выведет:
[5, 6]— только элементы длиннее 4 км остались в списке;filter, в отличие отmap, не меняет сами значения, а решает, какие из них оставить.
Когда у лямбды не один параметр, а несколько, it уже не подходит — нужно назвать каждый явно и разделить их запятой перед стрелкой ->.
fun main() {
val add: (Int, Int) -> Int = { a, b -> a + b }
println(add(3, 4))
}
Разбор по строкам
- Строка 2:
val add: (Int, Int) -> Int = { a, b -> a + b }— слева от=объявлена переменнаяaddс типом(Int, Int) -> Int: это тип функции (function type), который читается «принимает два Int, возвращает Int». Справа — сама лямбда:a, b— имена двух параметров,->отделяет их от тела,a + b— тело. - Строка 3:
add(3, 4)— лямбда, лежащая в переменной, вызывается точно так же, как обычная функция: именем и скобками с аргументами.
Что выведет: 7 — потому что 3 + 4 = 7.
Функции высшего порядка: map, filter и свои
Функция высшего порядка (higher-order function) — это функция, которая принимает другую функцию как параметр или возвращает функцию. map и filter из главы 1 — как раз такие: они принимают лямбду и решают, что с ней делать для каждого элемента.
fun main() {
val distances = listOf(3, 5, 4, 6, 2)
val longInMeters = distances.filter { it > 4 }.map { it * 1000 }
println(longInMeters)
}
Что выведет: [5000, 6000] — сначала filter оставил тренировки длиннее 4 км ([5, 6]), затем map перевёл их в метры. Каждый метод в цепочке — сам по себе функция высшего порядка: и filter, и map принимают лямбду параметром.
Посмотри в замедленной съёмке, как выполнение прыгает из main в функцию и обратно, а map прогоняет лямбду по каждому элементу:
1fun toMeters(km: List<Int>): List<Int> {2return km.map { it * 1000 }3}45fun main() {6val distances = listOf(3, 5, 4)7val meters = toMeters(distances)8println(meters)9}
Нажимай «Шаг» и следи, как выполнение прыгает из main в функцию и обратно, а map прогоняет лямбду по каждому элементу.
Теперь напишем свою функцию высшего порядка — такую же, как filter/map внутри устроены сами.
fun main() {
println(calculate(6, 3, { a, b -> a + b }))
println(calculate(6, 3) { a, b -> a - b })
}
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
Разбор по строкам
- Строка 5: сигнатура
calculate— разобрана выше по символам. - Строка 6:
return operation(a, b)— внутриcalculateмы не знаем заранее, что за операция придёт: сложение, вычитание, что угодно.operation(a, b)вызывает то, что передали, как обычную функцию. - Строка 2:
calculate(6, 3, { a, b -> a + b })— лямбда передана третьим аргументом обычным способом, в скобках через запятую. - Строка 3:
calculate(6, 3) { a, b -> a - b }— то же самое, но лямбда вынесена за скобки вызова. Такая запись называется трейлинг-лямбдой (trailing lambda): если лямбда — последний параметр функции, её можно писать не внутри( ), а сразу после них.
Что выведет:
9
3
Первый вызов сложил (6 + 3 = 9), второй вычел (6 − 3 = 3) — потому что мы передали разные лямбды одной и той же функции calculate.
А если той же calculate с теми же 6 и 3 передать третью лямбду — с умножением?
fun main() {
println(calculate(6, 3) { a, b -> a * b })
}
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}Что напечатает этот код? Сначала ответь, потом проверяй.
Теперь собери сигнатуры сам — от простой функции до функции высшего порядка:
Задание 1 из 3. Функция greet: принимает имя-строку, возвращает строку.
Типичные ошибки
Все четыре ошибки ниже — реальные сообщения из встроенной песочницы Kotlin Playground (той же, что работает в кнопках Run выше в этой главе). Тексты проверены вживую в этой сессии.
1. Значение из Unit-функции
fun main() {
println(square(4))
}
fun square(x: Int) {
return x * x
}
Return type mismatch: expected 'Unit', actual 'Int'.
Перевод: «Несовпадение типа возврата: ожидался Unit, а по факту Int». У square не указан тип возврата — значит, Kotlin считает его Unit («ничего»). А return x * x пытается вернуть Int. Обещание (неявное Unit) не совпало со значением.
Как починить: указать настоящий тип возврата — fun square(x: Int): Int { return x * x }, — либо, если функция правда ничего не должна возвращать, убрать return x * x и оставить только действие.
2. Забытый return в теле-блоке
fun main() {
println(square(4))
}
fun square(x: Int): Int {
x * x
}
Missing return statement.
Перевод: «Отсутствует оператор return». Функция обещала вернуть Int (: Int в сигнатуре), но тело-блок сам по себе ничего не возвращает — x * x здесь просто вычисляется и тут же забывается. В отличие от тела-выражения, тело-блок никогда не возвращает своё последнее выражение автоматически.
Как починить: добавить return явно — return x * x, — либо переписать функцию как тело-выражение: fun square(x: Int): Int = x * x.
3. Не передан обязательный аргумент
fun greet(name: String, greeting: String) {
println("$greeting, $name!")
}
fun main() {
greet("Олег")
}
No value passed for parameter 'greeting'.
Перевод: «Не передано значение для параметра greeting». У greeting нет значения по умолчанию, поэтому он обязателен, а вызов greet("Олег") передаёт только один аргумент.
Как починить: передать оба аргумента — greet("Олег", "Привет"), — либо дать greeting значение по умолчанию в объявлении: greeting: String = "Привет".
4. Лямбда filter вернула не Boolean
fun main() {
val numbers = listOf(1, 2, 3)
val big = numbers.filter { it }
println(big)
}
Return type mismatch: expected 'Boolean', actual 'Int'.
Перевод: «Несовпадение типа возврата: ожидался Boolean, а по факту Int». Лямбда, переданная в filter, обязана отвечать на вопрос «оставить этот элемент?» — то есть возвращать Boolean (true/false). Здесь лямбда { it } просто отдаёт сам элемент, а он Int, а не Boolean.
Как починить: написать настоящее условие, дающее Boolean, например filter { it > 0 }.
Символы главы
Шпаргалка по всем новым знакам и конструкциям этой главы.
| Символ / конструкция | Как называется | Что делает | Пример |
|---|---|---|---|
fun | ключевое слово | объявляет функцию | fun square(x: Int): Int |
( ) после имени функции | список параметров | объявляет входные данные функции | square(x: Int) |
: после скобок функции | аннотация типа возврата | указывает, что функция возвращает | ): Int { |
return | ключевое слово | возвращает значение из тела-блока и завершает функцию | return x * x |
Unit | тип «ничего» | возвращаемый тип функций, которые не возвращают значение; пишется неявно, если : Тип пропущен | fun greet(name: String) { ... } |
= после сигнатуры функции | тело-выражение | заменяет { return ... } на одну строку; тип возврата можно вывести | fun square(x: Int) = x * x |
= значение внутри скобок параметра | параметр по умолчанию | делает аргумент необязательным при вызове | greeting: String = "Привет" |
имя = значение при вызове | именованный аргумент | явно указывает, какому параметру передаётся значение, независимо от порядка | describe(score = 42, name = "Соня") |
{ a, b -> ... } | лямбда с именованными параметрами | стрелка отделяет параметры лямбды от тела | { a, b -> a + b } |
it | неявный параметр лямбды | имя единственного параметра, если его не назвали явно | filter { it > 4 } |
(Тип, Тип) -> Тип | тип функции | описывает сигнатуру лямбды/функции как значения | (Int, Int) -> Int |
fn(a, b) { ... } | трейлинг-лямбда | лямбда-последний-параметр выносится за круглые скобки вызова | calculate(6, 3) { a, b -> a - b } |
Углубиться
Unit — не пустота, а настоящий тип. В отличие от void в некоторых других языках, Unit в Kotlin — обычный тип с единственным значением (тоже называется Unit). Поэтому функцию, возвращающую Unit, формально можно использовать всюду, где ожидается значение, — например, передать её результат в println, и это не будет ошибкой. На практике так почти никогда не делают: Unit пишут (точнее, не пишут явно) для функций-действий, у которых важен побочный эффект, а не результат.
Замыкание (closure). Лямбда умеет обращаться не только к своим параметрам, но и к переменным, объявленным снаружи, — и даже изменять var-переменные, если они var. Например: var sum = 0; numbers.forEach { sum += it } — лямбда внутри forEach видит и меняет внешнюю sum. Это называется замыканием: лямбда «замыкает» вокруг себя часть окружающего кода. Работает это не только для лямбд, но и для локальных функций.
Дальше по теме:
Слова главы
Потренируйся печатать
Сначала однострочная функция — тело-выражение:
Цель: скорость ≥ 110 зн/мин, точность ≥ 90%
fun square(x: Int): Int = x * x
Теперь лямбда с типом функции и двумя именованными параметрами:
Цель: скорость ≥ 120 зн/мин, точность ≥ 90%
val add: (Int, Int) -> Int = { a, b -> a + b }
Куда дальше: проверенные ресурсы
Русские материалы с нуля
- kotlinlang.ru — Функции — официальная документация в переводе: параметры по умолчанию, именованные аргументы, тело-выражение разобраны с примерами.
- kotlinlang.ru — Лямбда-выражения и функции высшего порядка — тот же официальный перевод про лямбды,
it, функциональные типы и трейлинг-лямбды. - Хабр: Kotlin для новичков — всё о функциях за 15 минут — продолжение статьи про переменные из главы 1, тот же цикл OTUS: декомпозиция, параметры по умолчанию, именованные аргументы.
- Урок 10: функции в Kotlin — продолжение курса с урока 1:
fun, параметры, возвращаемый тип на практических примерах. - Hyperskill: Kotlin Functions — та же площадка, что и в главе 1, теперь тема — функции, именованные аргументы и значения по умолчанию.
Официальный тур Kotlin (на английском)
- Kotlin tour: Functions — официальный тур: сигнатура, именованные аргументы, значения по умолчанию, однострочные функции и лямбды со встроенными упражнениями и готовыми решениями.
Потренироваться
-
Kotlin Koans: Named arguments / Default arguments / Lambdas — в разделе Introduction есть три задачи именно на темы этой главы, с автопроверкой.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Челлендж ⭐
Та же команда из главы 1 пробежала за неделю пять тренировок: listOf(3, 5, 4, 6, 2) километров.
Ступень 1. Напиши функцию totalLong(distances: List<Int>, threshold: Int = 4): Int, которая суммирует в метрах только те тренировки, что длиннее threshold километров. Используй цепочку .filter { }.map { }.sum() из этой главы и объяви функцию как тело-выражение (=, без фигурных скобок и return). Значение по умолчанию для threshold — 4.
Ступень 2 ⭐. Напиши функцию высшего порядка countBy(distances: List<Int>, predicate: (Int) -> Boolean): Int, которая возвращает количество тренировок, удовлетворяющих условию predicate. Вызови её с трейлинг-лямбдой, чтобы посчитать, сколько тренировок длиннее 4 км.
fun main() {
val distancesKm = listOf(3, 5, 4, 6, 2)
// ступень 1: замени 0 на вызов своей функции totalLong
val totalLongMeters = 0
println("Метров в длинных тренировках: $totalLongMeters")
// ступень 2 (со звёздочкой): напиши countBy и вызови её с трейлинг-лямбдой
}
// ступень 1: допиши функцию здесь
// fun totalLong(distances: List<Int>, threshold: Int = 4): Int = ...
// ступень 2 (со звёздочкой): допиши функцию здесь
// fun countBy(distances: List<Int>, predicate: (Int) -> Boolean): Int = ...
Решение (сначала попробуй сам)
Ступень 1:
fun totalLong(distances: List<Int>, threshold: Int = 4): Int =
distances.filter { it > threshold }.map { it * 1000 }.sum()
Ступень 2 ⭐:
fun countBy(distances: List<Int>, predicate: (Int) -> Boolean): Int =
distances.filter(predicate).size
fun main() {
val distancesKm = listOf(3, 5, 4, 6, 2)
val totalLongMeters = totalLong(distancesKm)
println("Метров в длинных тренировках: $totalLongMeters")
val longCount = countBy(distancesKm) { it > 4 }
println("Длинных тренировок: $longCount")
}
Полный вывод:
Метров в длинных тренировках: 11000
Длинных тренировок: 2
5 и 6 км — единственные тренировки длиннее 4 км: (5 + 6) * 1000 = 11000 метров, и их ровно две.
Что должен уметь
- Объявлять функцию с
fun, параметрами и типом возврата, объясняя, что делает каждый символ сигнатуры. - Отличать тело-блок (
{ return ... }) от тела-выражения (= ...) и знать, когда тип возврата можно не писать. - Объяснять, что такое
Unit, и когда тип возврата опускают. - Задавать параметру значение по умолчанию и предсказывать, что произойдёт при вызове без этого аргумента.
- Вызывать функцию с именованными аргументами — в произвольном порядке и пропуская параметры со значением по умолчанию.
- Писать лямбду и с неявным параметром
it, и с явно названными параметрами через->. - Объяснять, что такое функция высшего порядка, и приводить пример на
filter/map. - Писать свою функцию, принимающую лямбду как параметр через функциональный тип
(Тип, Тип) -> Тип, и вызывать её через трейлинг-лямбду. - Узнавать по сообщению компилятора четыре типичные ошибки функций и лямбд — значение из Unit-функции, забытый
return, непереданный обязательный аргумент, неверный тип результата лямбды — и чинить каждую.
Комментарии
Комментарии появятся после настройки. Нужен аккаунт GitHub — вход прямо в виджете выше.