1. Вступ
Уявіть, що ви пишете програму, яка просить користувача ввести вік. Користувач вводить "abc". Ви відповідаєте "некоректно". І… що далі? Якщо у вас є лише if, програма просто дійде до кінця й завершиться, залишивши користувача з відчуттям: «ну й гаразд тоді». Цикли потрібні саме для цього — навчити програму повторювати дію, доки не буде досягнуто потрібного результату. Або, якщо сказати інакше, доки умова істинна.
У реальному житті цикли трапляються всюди: повторне введення, підрахунки «від 1 до N», накопичення суми, поступове наближення до цілі. І так, цикли — головний інструмент, за допомогою якого можна випадково влаштувати нескінченність (програмісти інколи теж люблять вічність, але компілятор — не завжди).
2. while: синтаксис і ментальна модель
Цикл while читається дуже по-людськи: «поки умова істинна — роби». У Kotlin він виглядає так:
while (condition) {
// тіло циклу
}
Головна особливість: умова перевіряється до входу в тіло. Це означає, що якщо умова одразу false, тіло не виконається жодного разу. І це не «помилка», а корисна поведінка. Саме тому while ідеально підходить для ситуацій на кшталт «виконуємо, доки має сенс».
while — це як охоронець на вході до клубу: «Є квиток?» (умова). Якщо квитка немає, усередину не впустять узагалі — ні на одну ітерацію.
Невелика блок-схема, щоб побачити рух програми буквально очима:
flowchart TD
A[Старт] --> B{condition?}
B -- ні --> E[Вихід із циклу]
B -- так --> C[Тіло циклу]
C --> D[Кінець ітерації]
D --> B
І так: Kotlin — це Kotlin, але while тут цілком класичний — «як усюди», без сюрпризів. Навіть в офіційних прикладах імперативного стилю (де використовують var і цикл) while виглядає саме так.
3. Шаблони while
Лічильник: три обовʼязкові частини
Коли новачки бачать цикл, перша думка часто така: «О, повторення! Зараз я повторю щось… і воно само зупиниться». Не зупиниться. Циклу потрібно допомогти.
У класичного циклу-лічильника є три частини, і вони майже завжди видимі прямо в коді:
- початкове значення (до циклу),
- умова продовження (у while (...)),
- зміна змінної, щоб умова колись стала хибною (усередині тіла).
Найпростіший приклад — друкуємо числа від 1 до 5:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i") // i = 1, потім 2, 3, 4, 5
i = i + 1
}
}
Зверніть увагу: змінна i оголошена як var, бо ми її змінюємо. Якби це було val, компілятор справедливо сказав би: «ви ж обіцяли не змінювати».
А тепер приклад із межею, де тіло може не виконатися жодного разу. Це важлива «перевірка на чесність» для вашого розуміння:
fun main() {
var i = 10
while (i <= 5) {
println("Це не надрукується")
i = i + 1
}
println("Цикл не запускався жодного разу") // Цикл не запускався жодного разу
}
Умова i <= 5 одразу false, тому всередину циклу ми не потрапили. Іноді це саме те, що потрібно. Наприклад: «повторювати, доки є помилки», а помилок може не бути вже на старті.
Накопичення: сума і максимум
Цикл while корисний не лише для друку «кроків», а й для накопичення результату. Типовий сценарій: «є кілька чисел — потрібно порахувати суму». Поки у нас немає масивів і списків, ми тренуємося на фіксованій кількості значень або на варіанті «вводимо N разів».
Почнемо із зовсім малого: сума від 1 до 3.
fun main() {
var sum = 0
var i = 1
while (i <= 3) {
sum = sum + i
i = i + 1
}
println("sum = $sum") // sum = 6
}
Тут дві ролі змінних дуже чіткі: i — «крок», sum — «накопичувач». Коли ви змішуєте ролі (наприклад, використовуєте одну змінну і як лічильник, і як суму), код стає схожим на жарт, який смішний лише автору.
Ще один корисний шаблон — знаходити максимум серед кількох введених значень. Так, пізніше ми зробимо це набагато зручніше, але зараз нам важлива логіка «порівнюємо й оновлюємо».
Припустімо, ми хочемо обробити 3 числа (введення вважаємо коректним, щоб не відволікатися від теми):
fun main() {
var max: Int? = null
var count = 0
while (count < 3) {
print("Введіть число: ")
val n = readln().trim().toInt()
if (max == null || n > max) max = n
count = count + 1
}
println("max = $max") // наприклад: max = 42
}
Тут max зроблено nullable (Int?), бо «максимуму» до першого введення ще немає. Це хороший приклад того, що nullable-типи — не лише для «страшних тем про null», а інколи просто чесний спосіб сказати: «значення зʼявиться пізніше».
Нескінченний цикл: чому він виникає і як його виявити
Час поговорити про головну розвагу початківця програміста: «я запустив програму, а вона зависла». Насправді вона часто не зависла — вона просто знайшла сенс життя й тепер нескінченно повторює одне й те саме.
Причина нескінченного циклу майже завжди одна: умова не стає false. Або ви забули змінити змінну, або змінюєте не ту, або змінюєте «не туди».
Найкласичніша помилка:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i")
// забули i = i + 1
}
}
Тут i завжди 1, умова завжди істинна, і програма друкуватиме "i = 1" доти, доки ви (або ваша операційна система) не скажете: «усе, досить».
Ще більш підступна версія — ви оновлюєте змінну, але так, що умова ніколи не стане хибною:
fun main() {
var i = 1
while (i <= 5) {
println("i = $i")
i = i - 1
}
}
i іде в 0, -1, -2… і умова i <= 5 залишається істинною назавжди.
Практичне правило «анти-нескінченність» звучить нудно, але працює залізно: після написання while очима знайдіть у тілі рядок, який впливає на умову. Якщо не знайшли — у вас майже напевно вічний двигун.
4. Повторне введення і перевірка
Лічильники — це добре, але одна з найжиттєвіших причин полюбити while — надійне введення. Користувач може ввести пробіли, порожній рядок, «сорок два» літерами й навіть "42.0" (а ви хотіли Int). І якщо ви використовуєте toInt(), програма може впасти. Тому для сценарію «повторювати введення до коректного значення» ми використовуємо звʼязку trim() + toIntOrNull().
readln() читає рядок повністю, а toIntOrNull() повертає або число, або null, якщо рядок — не число. Це ідеально лягає на while.
Приклад: просимо вік (ціле число від 0 до 130) і повторюємо, доки не отримаємо нормальний результат.
fun main() {
var age: Int? = null
while (age == null) {
print("Введіть вік (0..130): ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
if (parsed != null && parsed in 0..130) {
age = parsed
} else {
println("Некоректне введення. Спробуйте ще раз.")
}
}
println("Вік прийнято: $age") // наприклад: Вік прийнято: 25
}
Чому це зручно:
- Ми не «падаємо» на поганому введенні — бо не використовуємо toInt() напряму.
- Умова циклу дуже читабельна: «доки вік не отримано».
- Усередині циклу чітко видно, що саме робить введення коректним: число має розпарситися й потрапити в діапазон.
Зверніть увагу на важливу дрібницю: перевірка parsed in 0..130 можлива лише якщо parsed != null. Тому ми пишемо parsed != null && … Це продовження тієї самої логіки, яку ви вже застосовували в умовах із && і ||.
5. Міні‑застосунок: BudgetBuddy v0
Зараз зберемо невеликий цілісний сценарій, який виглядатиме як «справжня програма», а не набір розрізнених прикладів. Нехай це буде BudgetBuddy v0: мініпомічник, який просить ввести три витрати й рахує суму. Головний фокус — цикл while і стійкість до неправильного введення без break/continue (їх ми ще не проходили).
Ідея така: нам потрібно рівно 3 коректні числа. Якщо користувач увів нісенітницю, ми не рухаємо лічильник і просимо знову. Це дуже важливий прийом: лічильник збільшується лише в разі успіху.
fun main() {
var sum = 0
var entered = 0
while (entered < 3) {
print("Введіть витрату №${entered + 1} (ціле число >= 0): ")
val raw = readln().trim()
val value = raw.toIntOrNull()
if (value != null && value >= 0) {
sum = sum + value
entered = entered + 1
} else {
println("Некоректно. Потрібне ціле число >= 0.")
}
}
println("Сума 3 витрат = $sum") // наприклад: Сума 3 витрат = 1200
}
Зверніть увагу, як акуратно розподілилися ролі:
entered відповідає лише за те, скільки коректних витрат ми прийняли. Якщо введення погане, entered не змінюється — і ми лишаємося на тому самому «номері витрати». Це і є «повторне введення» в реальному житті, без дублювання коду.
І ще одна деталь про читабельність: "№${entered + 1}" — це рядковий шаблон, який ви вже знаєте. Він робить підказку дружньою: користувач розуміє, що вводить «витрату №2», а не просто якісь абстрактні числа в порожнечу.
6. Типові помилки під час роботи з while
Помилка № 1: забули змінити змінну, від якої залежить умова.
Найчастіша причина нескінченного циклу — змінна в умові (i, entered, age) лишається незмінною. Корисна звичка: одразу після написання while (...) очима знайти в тілі рядок, який зрештою робить умову хибною. Якщо такого рядка немає, цикл, найімовірніше, буде вічним.
Помилка № 2: оновлюєте «не туди», і умова ніколи не стане хибною.
Іноді оновлення є, але логіка неправильна. Наприклад, у циклі while (i <= 5) ви робите i = i - 1. Формально програма «щось змінює», але за змістом вона рухається в бік, де умова лишається істинною нескінченно. Коли помічаєте, що значення «тікає» від межі, зупиніться й перевірте знак зміни.
Помилка № 3: змішали ролі змінних, і цикл перестав бути зрозумілим.
Якщо однією змінною ви і рахуєте кроки, і зберігаєте суму, і ще намагаєтеся тримати «останнє введення», код стає важко читати й легко зламати. Хороший стиль для новачка — «одна змінна = одна роль»: окремо лічильник, окремо накопичувач, окремо розпарсене значення.
Помилка № 4: використовуєте toInt() у циклі введення й ловите падіння програми.
toInt() хороший, коли ви впевнені у форматі введення. Але в задачах «введення від користувача» впевненості зазвичай немає. Тому для повторного введення краще використовувати toIntOrNull() і перевірку на null, як ми зробили вище. Так програма не падає й може ввічливо попросити повторити введення.
Помилка № 5: порівнюєте або перевіряєте діапазон для значення, яке може бути null.
Типова ситуація: val x = raw.toIntOrNull() — і далі спроба зробити if (x >= 0). Компілятор Kotlin не дасть так написати (і матиме рацію), бо x може бути null. Правильний порядок думок такий: спочатку «не null?», потім «чи підходить діапазон?». Це особливо важливо в циклах, бо помилка в перевірці часто перетворюється на нескінченне повторення або на неможливість прийняти коректне введення.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ