1. Вступ
Іноді здається, що while — це «універсальний цикл» і на ньому можна зупинитися. Але в реальних програмах (навіть у найпростіших консольних) часто трапляється сценарій, коли дія має відбутися хоча б один раз. Наприклад, ми маємо запитати в користувача команду або число щонайменше один раз — інакше в нас просто немає даних, щоб щось перевіряти.
І ось тут while починає змушувати писати «незграбний ритуал»: спочатку виконати дію один раз до циклу, а потім повторювати цю саму дію усередині циклу. Це не критично, але дратує — як коли ви двічі перевіряєте, чи зачинили двері, і щоразу сумніваєтеся.
do-while якраз і існує для таких випадків: «виконай тіло щонайменше один раз, а потім уже подивися на умову».
З погляду Kotlin перевірка умови у while відбувається до виконання тіла, а у do-while — після нього. Тому тіло do-while виконується щонайменше один раз.
Синтаксис do-while і «людська» модель
Коли ви вперше бачите do-while, він може здаватися трохи незвичним: умова не зверху, а знизу. Але логіка дуже проста: спочатку робимо крок, а потім запитуємо себе: «Ще раз?».
Синтаксис:
do {
// тіло циклу
} while (condition)
Це буквально читається як фраза: «Зроби { … } і повторюй, доки condition істинне».
Важливо запамʼятати одне правило — воно добре рятує від плутанини: у do-while умова відповідає за повторення наступної ітерації, а не за “дозвіл” першої. Перша ітерація відбудеться завжди — адже «спочатку зробити» вже сталося.
2. while і do-while на одній задачі
Дуже корисно побачити різницю не лише «в теорії», а на одному й тому самому сценарії. Візьмемо задачу: користувач має ввести слово "exit", щоб завершити роботу. Але запитати його ми маємо хоча б один раз.
Варіант на while із дублюванням
fun main() {
print("Введіть команду (exit для виходу): ")
var cmd = readln().trim().lowercase()
while (cmd != "exit") {
print("Введіть команду (exit для виходу): ")
cmd = readln().trim().lowercase()
}
println("Завершення.") // Завершення.
}
Цей код працює, але зверніть увагу: рядок із print(...) і рядок із читанням readln() трапляються двічі. Новачки інколи кажуть: «Ну й гаразд». А потім настає момент, коли ви змінюєте текст підказки й забуваєте змінити його в одному місці. І програма починає поводитися так, ніби в неї роздвоєння особистості.
Варіант на do-while без дублів
fun main() {
var cmd: String
do {
print("Введіть команду (exit для виходу): ")
cmd = readln().trim().lowercase()
} while (cmd != "exit")
println("Завершення.") // Завершення.
}
Сенс той самий, але «ритуал дублювання» зник. Це і є головний практичний бонус do-while.
3. Повторне введення числа без дублювання
Зараз зробимо рівно те, за що do-while особливо люблять у консольних програмах: надійне введення «доти, доки не введуть правильно».
Нехай нам потрібне число від 1 до 5. Користувач може ввести будь-що: 3, " 3 ", "три", порожній рядок, 999. Наша програма має залишатися спокійною й просто повторювати запит.
Тут ми використовуємо вже знайомий шаблон: toIntOrNull() повертає Int?, тобто або число, або null.
Введення числа в діапазоні 1..5 через do-while
fun main() {
var number: Int? = null
do {
print("Введіть ціле число від 1 до 5: ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
number = if (parsed != null && parsed in 1..5) parsed else null
if (number == null) println("Некоректно.") // Некоректно.
} while (number == null)
println("Прийнято: $number") // Прийнято: 4
}
Тут є важлива ідея, яка робить код читабельним: змінна number зберігає «прийняте» значення. Поки значення не прийнято, там null. Цикл повторюється, доки number == null.
4. Область видимості в do-while
Коли ви пишете do-while, виникає цілком нормальне питання: «А де оголошувати змінну, яку перевіряємо в умові?». Відповідь проста: так, щоб вона була видима і в тілі, і в умові while (...).
Тому такий шаблон трапляється постійно:
fun main() {
var cmd: String
do {
cmd = readln().trim()
} while (cmd != "exit")
}
Якщо оголосити val cmd усередині блоку do { ... }, то зовні (у while (cmd != ...)) вона вже не буде видима — і компілятор скаже «не знаю такого імені». Це не примха Kotlin, а звичайне правило області видимості: імʼя «живе» всередині блоку, у якому його оголошено.
Щоб закріпити, ось приклад «як не можна» (він не скомпілюється):
fun main() {
do {
val cmd = readln().trim()
} while (cmd != "exit") // Помилка: cmd не видима тут
}
5. Корисні нюанси do-while
Мінісхема: як do-while виконується крок за кроком
Щоб менше плутатися, корисно тримати в голові невелику схему виконання. Уявімо, що в нас цикл повторного введення числа (доки number == null).
flowchart TD
A["START"] --> B["do { ... } виконується (мінімум 1 раз)"]
B --> C{"умова while (...)?"}
C -->|true| B
C -->|false| D["цикл завершено"]
D --> E["END"]
Порівняйте з while: там перевірка до тіла, тому можливий сценарій «0 ітерацій». А в do-while він неможливий: перша ітерація буде завжди.
Як писати читабельну умову
do-while легко перетворити на монстра, якщо писати в while (...) умову з пʼяти перевірок, із дужками, &&, || і спробами «все вмістити в один рядок». На цьому етапі варто памʼятати: читабельність — це частина коректності.
Якщо умова стає важкою, простіше зробити так: у тілі циклу обчислити «прийнято/не прийнято» в окрему змінну, а вже її використати в while.
Ось приклад того самого підходу, але трохи більш «розмовно»:
fun main() {
var age: Int? = null
do {
print("Вік (0..120): ")
val parsed = readln().trim().toIntOrNull()
val ok = parsed != null && parsed in 0..120
age = if (ok) parsed else null
if (!ok) println("Введіть ціле число від 0 до 120.")
} while (age == null)
println("Вік прийнято: $age") // Вік прийнято: 42
}
Код став довшим на один рядок, зате мозку легше.
Мікророзбір за ітераціями: що відбувається зі змінними
Іноді корисно «прокрутити» цикл у голові, ніби ви налагоджуєте програму вручну. Візьмемо введення віку й припустімо, що користувач увів: "abc", потім "-5", потім "30".
Усередині тіла на кожній ітерації відбувається приблизно таке:
| Ітерація | raw | parsed | age після присвоєння | Умова age == null |
|---|---|---|---|---|
| 1 | |
|
|
true (повторюємо) |
| 2 | |
|
|
true (повторюємо) |
| 3 | |
|
|
false (виходимо) |
Тобто do-while тут робить дуже зрозумілу роботу: «намагайтеся прийняти значення, доки не вийде».
6. Анкета v0.4: імʼя і вік
Щоб приклади не були набором окремих шматочків, давайте розвиватимемо один міні-застосунок. Нехай це буде «анкета»: користувач вводить імʼя і вік. Ми не робимо функції (крім main) і не використовуємо масиви — усе на рівні поточного дня.
Ідея така: імʼя має бути непорожнім (після trim()), а вік — цілим числом від 0 до 120. Ми хочемо мінімально «діставати» користувача, але водночас не приймати сміття.
Крок 1: надійне введення імені
fun main() {
var name: String
do {
print("Введіть імʼя: ")
name = readln().trim()
if (name.isEmpty()) println("Імʼя не повинно бути порожнім.") // Імʼя не повинно бути порожнім.
} while (name.isEmpty())
println("Привіт, $name!") // Привіт, Анна!
}
Зверніть увагу: тут у нас немає null, тому що readln() завжди повертає рядок. Нам достатньо перевіряти isEmpty() після trim().
Крок 2: надійне введення віку
Тепер додамо вік. Тут ми використовуємо Int?, тому що toIntOrNull() може повернути null.
fun main() {
var name: String
do {
print("Введіть імʼя: ")
name = readln().trim()
if (name.isEmpty()) println("Імʼя не повинно бути порожнім.")
} while (name.isEmpty())
var age: Int? = null
do {
print("Введіть вік (0..120): ")
val raw = readln().trim()
val parsed = raw.toIntOrNull()
age = if (parsed != null && parsed in 0..120) parsed else null
if (age == null) println("Вік введено некоректно.") // Вік введено некоректно.
} while (age == null)
println("Користувач: $name, вік: $age") // Користувач: Анна, вік: 25
}
Технічно ми могли б зберігати age як Int і окремо мати прапорець «коректно/некоректно». Але на цьому етапі Int? часто виходить простіше: null = «ще не прийняли».
7. Типові помилки під час роботи з do-while
Помилка №1: очікувати, що цикл може не виконатися жодного разу.
Це звичка після while. У do-while тіло виконається щонайменше один раз — навіть якщо умова спочатку «хибна». Тому do-while не підходить для ситуацій, де дія може бути не потрібна взагалі. Якщо для вас нормально, що «0 ітерацій — це ок», частіше краще підійде while.
Помилка №2: оголосити змінну всередині do {}, а використовувати її в while (...).
Це класична проблема області видимості. Якщо змінна оголошена всередині блоку, зовні вона не видима, і умова не скомпілюється. Майже завжди змінну, що бере участь в умові, оголошують до циклу — щоб вона була доступна в обох місцях.
Помилка №3: використовувати toInt() замість toIntOrNull() у сценарії повторного введення.
toInt() під час введення на кшталт "abc" завершить програму винятком, а нам у навчальних сценаріях потрібно навпаки — мʼяко сказати «некоректно» і повторити. Для надійного введення краще toIntOrNull(), бо він перетворює нечислове введення на null.
Помилка №4: забути нормалізувати рядок (trim()) перед перевірками.
Користувач легко введе " exit " або " 25 ". Без trim() такі значення часто «не збігатимуться» з очікуваними, і програма вперто робитиме вигляд, що користувач помиляється, хоча він просто поставив пробіли. Це особливо помітно в командах ("exit") і числах.
Помилка №5: надто складна умова в while (...) і втрата сенсу.
Якщо ви пишете внизу циклу вираз на пів екрана, ви майже гарантовано колись помилитеся в дужках або логіці. На цьому рівні краще обчислювати умову «прийнято/не прийнято» всередині тіла в окремий val ok, а потім керувати повторенням через зрозумілу перевірку на кшталт while (!ok) або while (age == null).
Помилка №6: змішувати в одній змінній різні ролі.
Наприклад, намагатися зберігати і «сирий рядок введення», і «число», і «чи прийнято значення» в одному var x. Виходить плутанина, а потім — баги, які складно пояснити навіть собі. Краще так: raw для рядка, parsed для результату парсингу, і окрема змінна для прийнятого значення (age) або для прапорця (ok).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ