1. Введение
Когда вы только начинаете программировать, самая естественная реакция на задачу «найти элемент» или «посчитать» — написать цикл и в нём делать проверки. Это нормально: мозг честно имитирует работу процессора. Но у такого подхода есть минусы: много шаблонного кода, легко ошибиться с break, легко забыть начальное значение счётчика, а иногда код получается таким, что его страшно трогать даже вам самим через два дня.
Операции find, any, all, count — это стандартные функции Kotlin для коллекций, которые выражают намерение напрямую: «найди», «проверь, что есть хотя бы один», «проверь, что все», «посчитай». Они читаются почти как русский текст, и код становится короче. А ещё это помогает отделять «мы считаем» от «мы печатаем», что делает программу предсказуемее.
Ниже будет много примеров. Мы начнём с очень маленьких списков, а потом применим всё к нашему консольному мини‑приложению учёта расходов, которое мы развиваем с дней про коллекции и команды.
2. find { ... }: найти первый подходящий элемент и подружиться с T?
find — это операция поиска. Вы даёте ей условие (лямбду‑предикат), и она возвращает первый элемент, который подходит. Если подходящего элемента нет, find возвращает null.
Это важный момент: тип результата будет nullable (T?), и компилятор Kotlin будет требовать, чтобы вы обработали ситуацию «ничего не нашли». Это не придирка, а забота: программа не должна падать от того, что элемент не нашёлся.
Начнём с простого примера на числах:
fun main() {
val xs = listOf(1, 3, 4, 6)
val firstEven: Int? = xs.find { it % 2 == 0 }
println(firstEven) // 4
}
Здесь firstEven — это Int?. Даже если вы глазами видите, что чётное точно есть, Kotlin всё равно заставляет вас жить по правилам: «может быть null». Это дисциплина, которая позже сэкономит вам часы отладки.
Теперь пример, где элемента нет:
fun main() {
val xs = listOf(1, 3, 5)
val firstEven = xs.find { it % 2 == 0 }
println(firstEven) // null
}
Обратите внимание: мы ничего не делали, чтобы «вернуть null». find сам так устроен: нет совпадений — получите null. Это и есть контракт.
find в CLI‑приложении: найти расход по id
Мы продолжаем условный CLI‑учёт расходов. По договорённости (пока без ООП) будем хранить один расход как Triple(id, category, amount), то есть Triple<Int, String, Int>. Это не идеальная модель, но сейчас нам важнее операции над коллекциями, а не красота доменной модели.
Сделаем функцию поиска расхода по id через find:
fun findExpenseById(
expenses: List<Triple<Int, String, Int>>,
id: Int
): Triple<Int, String, Int>? {
return expenses.find { expense -> expense.first == id }
}
Теперь в месте, где нам нужно удалить расход, мы не обязаны вручную бегать по списку.
fun removeExpenseById(
expenses: MutableList<Triple<Int, String, Int>>,
id: Int
): Boolean {
val found = expenses.find { it.first == id } ?: return false
expenses.remove(found)
return true
}
Здесь происходит аккуратная штука: если find вернул null, Elvis‑оператор ?: сразу делает return false. Это читается почти как разговор с программой: «если не нашли — сразу сообщи, что удалить не получилось».
3. any { ... } и all { ... }: проверки «есть хотя бы один» и «все подходят»
Иногда нам не нужен сам элемент. Нам нужен ответ «да/нет»: есть ли среди элементов такие, которые удовлетворяют условию? Или наоборот: все ли элементы удовлетворяют условию? Раньше вы бы заводили флаг var ok = false и меняли его в цикле. Kotlin говорит: «не мучайся» — есть any и all.
any { predicate } возвращает true, если хотя бы один элемент подходит под условие. all { predicate } возвращает true, если все элементы подходят под условие. Это две операции, которые очень любят жить в валидации и быстрых проверках «можно ли продолжать».
Вот пример:
fun main() {
val words = listOf("a", "bb", "ccc")
println(words.any { it.length == 2 }) // true
println(words.all { it.isNotEmpty() }) // true
}
Важный крайний случай: пустая коллекция
Самый неожиданный момент (и классическая ловушка новичка): поведение any и all на пустой коллекции.
fun main() {
val empty = emptyList<Int>()
println(empty.any { it > 0 }) // false
println(empty.all { it > 0 }) // true
}
Это не баг и не «странность Kotlin». Это логика из математики:
— any на пустом наборе — false, потому что «нет ни одного элемента, который подтвердил бы условие».
— all на пустом наборе — true, потому что «нет ни одного контрпримера». Никто не нарушил правило — формально правило выполнено.
Чтобы это было проще запомнить, держите маленькую таблицу:
| Коллекция | Проверка | Результат | Интуиция |
|---|---|---|---|
|
|
|
«Никого нет — значит, никто не подходит» |
|
|
|
«Никого нет — значит, никто не нарушил условие» |
Когда это важно? Например, если вы проверяете «все суммы положительные». Если список пуст, all скажет true. Иногда это то, что вам нужно (пустой список не содержит ошибок). А иногда вы хотите отдельно проверить «список не пуст», но это уже часть бизнес‑логики, а не проблема all.
any/all в CLI‑приложении: быстрые проверки данных
Допустим, мы хотим понять: есть ли среди расходов очень крупные траты (скажем, больше 10_000). Это чистый any:
fun hasBigExpenses(
expenses: List<Triple<Int, String, Int>>
): Boolean {
return expenses.any { it.third > 10_000 }
}
И наоборот: хотим проверить, что все суммы корректные (например, строго больше нуля). Это all:
fun allExpensesPositive(
expenses: List<Triple<Int, String, Int>>
): Boolean {
return expenses.all { it.third > 0 }
}
Здесь важно держать в голове смысл: any/all — это не «похоже на if», это «ответ на вопрос». И чем точнее ваш вопрос, тем чище код.
4. count { ... }: подсчёт без внешнего счётчика
count — это операция, которая возвращает число элементов в коллекции. У неё есть два режима.
Если вызвать count() без условий — вы получите размер коллекции (то же, что size, но иногда удобно именно как «операция» в цепочке рассуждений). Если вызвать count { predicate }, вы получите количество элементов, удовлетворяющих условию. Kotlin‑документация относит count к агрегирующим операциям: возвращаем одно значение по содержимому коллекции.
Мини‑пример:
fun main() {
val xs = listOf(1, 2, 3, 4, 5)
val evenCount = xs.count { it % 2 == 0 }
println(evenCount) // 2
}
Почему count { ... } лучше, чем filter { ... }.size
Да, можно написать так:
val evenCount = xs.filter { it % 2 == 0 }.size
Это будет работать. Но по смыслу вы делаете «фильтрацию» (получаете список), а потом берёте его размер. А вы ведь не хотели список — вы хотели число. count выражает намерение точнее и читается проще: «посчитай элементы, подходящие под условие». Плюс вы не создаёте промежуточный список (это уже вопрос эффективности, но даже без углубления приятно знать, что Kotlin за вас не делает лишней работы).
count в CLI‑приложении: простая статистика
Сделаем пару функций, которые считают, сколько у нас расходов в конкретной категории, и сколько всего:
fun totalExpenseCount(expenses: List<Triple<Int, String, Int>>): Int {
return expenses.count()
}
fun countByCategory(
expenses: List<Triple<Int, String, Int>>,
category: String
): Int {
return expenses.count { it.second == category }
}
Обратите внимание: count() возвращает Int, а count { ... } тоже возвращает Int. То есть это «финальный результат», который можно печатать, сохранять, сравнивать в if, и так далее.
5. Практический пример: команды remove и stats в CLI
Теперь сделаем кусок «практического» кода для нашего консольного приложения: добавим команду удаления по id через find, и команду stats, которая показывает несколько ответов через count/any/all. Важно: мы не строим идеальный проект, а тренируем операции коллекций, поэтому код будет максимально прямолинейным и небольшими кусками.
Печать одного расхода
Сделаем утилиту печати расхода:
fun printExpense(expense: Triple<Int, String, Int>) {
val id = expense.first
val category = expense.second
val amount = expense.third
println("#$id | $category | $amount")
}
Команда remove: «нашёл → удалил» без ручного обхода
fun handleRemove(
expenses: MutableList<Triple<Int, String, Int>>,
id: Int
) {
val found = expenses.find { it.first == id }
if (found == null) {
println("Расход с id=$id не найден")
return
}
expenses.remove(found)
println("Удалено:")
printExpense(found)
}
Тут сразу несколько полезных мыслей.
Во-первых, find даёт Triple<...>?, и мы честно проверяем null. Это как раз то, что в Kotlin называется «пишем код, который выживает в реальном мире».
Во-вторых, удаляем мы объектом found, а не индексом. Для MutableList это нормальный и понятный путь: нашли элемент — удалили этот элемент.
Команда stats: считаем и проверяем без циклов
fun printStats(expenses: List<Triple<Int, String, Int>>) {
val total = expenses.count()
val foodCount = expenses.count { it.second == "food" }
val hasNegative = expenses.any { it.third < 0 }
val allPositive = expenses.all { it.third > 0 }
println("Всего расходов: $total")
println("В категории food: $foodCount")
println("Есть отрицательные суммы: $hasNegative")
println("Все суммы положительные: $allPositive")
}
Заметьте, как этот код читается: он почти как текст отчёта. И самое приятное — здесь вообще не нужно заводить var counter, не нужно помнить про break, не нужно писать четыре цикла подряд.
Полезный приём: найти проблемный элемент через find
Если allPositive вдруг false, обычно хочется понять «а что именно сломано?». Для этого удобно сделать find на то же условие, только наоборот:
fun printFirstNegative(expenses: List<Triple<Int, String, Int>>) {
val bad = expenses.find { it.third < 0 }
if (bad != null) {
print("Первый подозрительный расход: ")
printExpense(bad)
}
}
any отвечает на вопрос «существует ли проблема», а find помогает показать «пример проблемы». Это очень жизненная связка: валидация + диагностика.
6. Типичные ошибки при использовании find/any/all/count
Ошибка №1: забыть, что find возвращает T?, и обращаться к результату как к non-null.
Самая частая история выглядит так: вы написали val x = list.find { ... }, а потом сразу println(x.first) (или x.length, если это строка). Компилятор не даст вам это сделать — и правильно. Нужно либо проверять x != null, либо использовать safe-call x?.first, либо задавать поведение по умолчанию через Elvis ?:. Когда вы привыкаете к этому, код становится устойчивее, а «случайные падения» исчезают как жанр.
Ошибка №2: путать find и filter, ожидая «все подходящие», а не «первый подходящий».
find возвращает один элемент (первый), а не список. Если вам нужно собрать все подходящие элементы, это уже стиль filter (мы это обсуждали в предыдущей лекции про map/filter). Ошибка обычно проявляется тихо: вы думаете, что нашли «все большие расходы», а на самом деле нашли только первый и успокоились. Это один из тех багов, которые особенно любят жить в отчётах.
Ошибка №3: удивляться поведению all на пустой коллекции.
Если у вас expenses пустой, expenses.all { it.third > 0 } вернёт true. Это корректно по контракту, но иногда не соответствует смыслу «у нас есть хоть какие-то данные». Если вам принципиально важно отличать «всё хорошо» от «у нас нет данных», добавляйте отдельную проверку на пустоту там, где формулируется бизнес‑правило. Важно понимать: all не «ошибается», он честно отвечает на вопрос, который вы задали.
Ошибка №4: использовать count там, где нужна проверка, и забывать про намерение.
Иногда пишут что-то вроде if (xs.count { it < 0 } > 0) ... Это работает, но читается тяжеловато: сначала считаем, потом сравниваем. Если вы по смыслу хотите «есть ли хоть один отрицательный», то any { it < 0 } выразит это проще. count хорош, когда вам реально нужно число.
Ошибка №5: пытаться внутри any/all делать побочные эффекты и потом удивляться логике.
Технически вы можете написать xs.any { println(it); it < 0 }, но получите «странный» эффект: any остановится на первом совпадении и распечатает не все элементы. Это не баг — это короткое замыкание (операции могут завершаться раньше, чем пройдут весь список). Поэтому печать и другие «действия» лучше держать отдельно, а any/all/find/count использовать как чистые вопросы к данным.
Ошибка №6: забывать, что count { ... } проходит по всей коллекции, а find/any/all могут остановиться раньше.
В небольших учебных примерах разницы не видно, но на больших списках может быть заметно: find/any/all часто завершаются на раннем совпадении или нарушении, а count обязан досчитать до конца. Даже если вы пока не думаете про производительность, полезно понимать «почему операция могла быть быстрее» — это помогает писать более осмысленный код.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ