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, оператор Елвіса ?: одразу робить 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, або задавати поведінку за замовчуванням через оператор Елвіса ?:. Коли ви звикаєте до цього, код стає стійкішим, а «випадкові падіння» зникають як жанр.
Помилка №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 зобовʼязаний дорахувати до кінця. Навіть якщо ви поки що не думаєте про продуктивність, корисно розуміти, «чому операція могла бути швидшою». Це допомагає писати осмисленіший код.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ