1. Вступ
Коли ви починаєте писати на Kotlin, він іноді здається суворим учителем: «Ні, так не можна. І так не можна. І це не можна. І взагалі — чому ви так дивитеся?» Насправді це не занудство, а захист від тихих помилок, які в інших мовах можуть непомітно змінювати сенс програми. У Kotlin майже будь-які перетворення числових типів виконуються явно — тобто ви самі пишете, чого хочете.
Уявіть: ви рахуєте знижку у відсотках, зберігаєте її як Double, а потім десь «випадково» перетворюєте на Int — і дробова частина зникає. Якщо мова робить це автоматично, ви можете тижнями жити з помилкою «знижка іноді дивна», доки не спіймаєте потрібний момент. Kotlin змушує вас написати .toInt() і тим самим ніби запитує: «Ви точно хочете викинути дріб? Справді точно-точно?»
Це правило стосується не лише дробів. Kotlin не любить «неявного розширення» на кшталт Int → Double, навіть якщо за змістом воно безпечне. Тому замість «магії» у нас є прості й читабельні методи: toDouble(), toFloat(), toInt(), а у світі символів — ще й звʼязок Char ↔ число.
2. Базові перетворення чисел
Коли ви бачите виклик на кшталт x.toInt(), корисно думати не «Kotlin щось там зробив», а «я попросив Kotlin виконати конкретну операцію». Це важлива звичка: якщо ви не формулюєте дію, потім буде складно пояснити собі (і наставнику), чому відповідь «попливла».
Нижче — проста табличка, яка допомагає тримати в голові суть базових перетворень:
| Перетворення | Приклад | Що відбувається | Де може бути втрата даних |
|---|---|---|---|
|
|
число стає дробовим, значення зберігається | зазвичай ні |
|
|
число стає дробовим, але Float менш точний | інколи так (на великих числах) |
|
|
дробова частина відкидається (не заокруглюється!) | так, завжди, якщо є дріб |
|
|
може стати Infinity або сильно спотворитися | так |
|
|
точність не «відновлюється», але тип стає Double | дані вже могли бути втрачені раніше |
Зараз ми не заглиблюємося в біти й будову памʼяті (це окрема тема), але практичний висновок простий: перетворення «вниз» за розміром типу (наприклад, Double → Int, Double → Float) — майже завжди потенційно небезпечне.
Подивіться, як це виглядає в коді.
fun main() {
val x = 10
val d = x.toDouble()
println("x = $x") // x = 10
println("d = $d") // d = 10.0
}
Тут усе чудово: Int → Double зберігає значення — просто додає «можливість мати дробову частину».
А тепер — приклад, де дані втрачаються:
fun main() {
val price = 199.99
val asInt = price.toInt()
println("price = $price") // price = 199.99
println("asInt = $asInt") // asInt = 199
}
Останнє нагадування: toInt() не заокруглює, а саме відрізає дробову частину (по суті, рухається до нуля). Якщо ви хочете заокруглити «по-людськи», зробіть це окремим кроком (ви вже знаєте round/floor/ceil з попередньої лекції), і лише потім приводьте тип.
3. Змішування типів і непомітна втрата сенсу
Коли ви пишете невеликі навчальні програми, може здаватися, що перетворення типів — це рідкість. Але щойно зʼявляються «справжні» обчислення (середнє, відсоток, імовірність, гроші), раптом виявляється, що половина помилок — це не if і не цикли, а неправильний тип у неправильному місці.
Дуже типовий сценарій: хочемо отримати середнє значення. У нас є сума (Int) і кількість (Int). Якщо поділити як Int / Int, дробова частина зникне. Тому потрібно явно перейти до Double хоча б в одній частині виразу.
fun main() {
val sum = 7
val count = 2
val avg1 = sum / count
val avg2 = sum.toDouble() / count
println("avg1 = $avg1") // avg1 = 3
println("avg2 = $avg2") // avg2 = 3.5
}
Формуйте програмістське мислення: ми не «виправляємо Kotlin», а задаємо правило обчислення. Хочете дріб — робіть дріб.
Ще один частий випадок — змішування Float і Double. Kotlin не любить, коли ви випадково перемішали два різні дробові типи. Зазвичай простіше обрати Double як основний, а Float використовувати лише тоді, коли є чітка причина.
4. Char: один символ і чому це корисно
Тепер переходимо до нового типу — Char. У Kotlin Char — це один символ, який записують в одинарних лапках: 'A', '7', '?'. Літерали Char записують в одинарних лапках, а спеціальні символи задають через escape-послідовності.
Чому це важливо? Тому що новачки часто плутають:
- '7' — символ сім (тип Char)
- "7" — рядок довжини 1 (тип String)
- 7 — число (тип Int)
І це три різні сутності. Іноді їх можна взаємно перетворювати, але автоматично Kotlin цього не робить, щоб ви ненароком не наробили дурниць (таке трапляється з усіма — особливо на старті).
Ще Char часто трапляється в рядках: рядок — це послідовність символів, і ви можете брати окремі символи за індексом.
Escape-символи: '\n', '\t' та інші
Після знайомства з рядками корисно зрозуміти, що деякі символи складно записати «як є». Тому існують escape-послідовності: '\n' — новий рядок, '\t' — табуляція тощо. Вони теж належать до Char.
fun main() {
val a = 'A'
val tab = '\t'
val nl = '\n'
println("[$a]") // [A]
println("x${tab}y") // x y
println("line1${nl}line2") // line1
// line2
}
Тут tab і nl — це саме символи, а не «магічні команди» для друку.
5. Char і числа: коди, toChar() та цифри
Зараз буде шматочок, який зробить вас трохи більш «компʼютерними шаманами». Ідея проста: кожен символ має числовий код. На JVM Char — це 16-бітний Unicode-символ. Це означає, що символ можна розглядати як число — і навпаки: число можна перетворити на символ (якщо воно потрапляє в допустимий діапазон).
У Kotlin у символу є властивість code, яка повертає числовий код символу (Int). А зворотна операція — toChar() у числа.
fun main() {
val ch: Char = 'A'
val code: Int = ch.code
val back: Char = code.toChar()
println("ch = $ch") // ch = A
println("code = $code") // code = 65
println("back = $back") // back = A
}
Це виглядає як фокус, але насправді тут просто два взаємні перетворення.
Найпопулярніший практичний приклад — цифри. Введення з клавіатури часто приходить як текст. Навіть якщо ви не парсите весь Int, іноді потрібно взяти одну цифру як символ, перевірити, що це цифра, і перетворити її на число.
Крім того, з кодами можна робити «зсуви»: 'A' + 1 → 'B', '0' + 7 → '7'. У Kotlin така ідея трапляється, наприклад, у задачах, де символи порівнюють і перетворюють на числа.
«Символ цифри» → число без магії
Перетворення '7' на число 7 — це класика. Новачки часто намагаються зробити щось на кшталт:
- "7".toInt() — це працює для рядка, але не для Char
- ('7').toInt() — так не можна, бо Char не перетворюється на «значення цифри» автоматично
- 7.toChar() — це взагалі в інший бік і дасть не '7', а символ із кодом 7 (а це керувальний символ)
Правильний підхід такий: якщо у нас є символ ch і ми впевнені, що він у діапазоні '0'..'9', то значення цифри дорівнює різниці кодів: digit = ch.code - '0'.code
Спочатку перевіримо, що це справді цифра.
fun main() {
val ch = '7'
val isDigit = ch >= '0' && ch <= '9'
val digit = if (isDigit) ch.code - '0'.code else -1
println("isDigit = $isDigit") // isDigit = true
println("digit = $digit") // digit = 7
}
Тут -1 — просто «сигнал помилки» в межах прикладу: якщо це не цифра, ми повертаємо значення, яке точно не може бути цифрою. Пізніше, коли в нас зʼявляться серйозніші підходи до помилок, ми робитимемо інакше, але поки цього достатньо.
Важливо помітити: такий стиль (порівняння символів, робота з діапазонами й перетворення через коди) — не вигадка заради краси. Він справді трапляється в реальному Kotlin-коді, особливо в алгоритмічних задачах.
6. Мінісценарій: генеруємо купон із літер і цифр
Уявімо, що ми робимо консольний мінізастосунок LuckyCoupon. Він показує користувачу знижку у відсотках і код купона формату A7Q2 (літера–цифра–літера–цифра). Жодних циклів — просто чотири кроки, щоб не відволікатися від суті.
Ключова ідея: щоб отримати випадкову літеру, беремо випадкове число 0..25, додаємо його до коду 'A' і перетворюємо назад у Char.
import kotlin.random.Random
fun main() {
val letter1 = ('A'.code + Random.nextInt(0, 26)).toChar() // 'A'..'Z'
val digit1 = ('0'.code + Random.nextInt(0, 10)).toChar() // '0'..'9'
val letter2 = ('A'.code + Random.nextInt(0, 26)).toChar()
val digit2 = ('0'.code + Random.nextInt(0, 10)).toChar()
val code = "$letter1$digit1$letter2$digit2"
println("Ваш купон: $code") // приклад: A7Q2
}
Зверніть увагу на дві важливі речі.
Перша: тут немає «магії перетворень». Ми явно працюємо з кодами через .code і toChar(). Це робить код трохи довшим, зате й набагато зрозумілішим: із числа зробили код символу, з коду зробили символ.
Друга: ми акуратно використовуємо межі nextInt(0, 26) і nextInt(0, 10). Верхня межа не включається, а отже 26 літер латинського алфавіту ми покрили рівно й без сюрпризів.
Знижка й місце, де часто губиться дріб
Нехай знижка обчислюється як Double (наприклад, вам випала «плаваюча» знижка). Для виводу ми хочемо відсоток як ціле число. Тут важливо визначитися: ми відріжемо дріб чи заокруглимо? Зараз покажемо саме втрату дробу, щоб ви бачили різницю.
fun main() {
val discount = kotlin.random.Random.nextDouble() * 30.0 // 0.0..30.0
val discountInt = discount.toInt() // дріб відрізали
println("discount = $discount") // discount = 12.345678...
println("discountInt = $discountInt%") // discountInt = 12%
}
Якщо ви хочете заокруглювати «за правилами», тепер ви знаєте: це потрібно зробити окремим кроком.
7. Схема-підказка щодо перетворень
Іноді корисно тримати в голові не окремі методи, а невеличкий «маршрут» розвʼязання. Ось проста блок-схема, яка допомагає не плутати «відрізання дробу» і «заокруглення», а також памʼятати, що символи живуть у світі кодів.
flowchart TD
A["Є значення"] --> B{Це число чи символ?}
B -->|Число| C{Потрібен інший числовий тип?}
C -->|Так| D["Явно: toDouble/toFloat/toInt"]
C -->|Ні| E["Залишаємо як є"]
D --> F{Переходимо з Double в Int?}
F -->|Так| G["Розуміємо: дріб буде втрачено"]
F -->|Ні| H["Зазвичай безпечніше"]
B -->|Символ Char| I["Беремо ch.code (Int)"]
I --> J{Хочемо цифру?}
J -->|Так| K["Перевіряємо '0'..'9' і віднімаємо '0'.code"]
J -->|Ні| L["Перетворюємо числа ↔ символи через toChar()"]
8. Типові помилки
Помилка №1: плутати '7', "7" і 7.
Це три різні типи: Char, String і Int. Якщо ви зберігаєте цифру як Char, це не число 7 і не рядок "7". Намагатися застосовувати до Char методи для рядків або очікувати, що Char сам «стане числом», — часта причина плутанини й дивних перетворень.
Помилка №2: вважати, що toInt() заокруглює.
toInt() не заокруглює, а відкидає дробову частину. Це особливо неприємно, коли ви очікуєте «знижка 12.9% стане 13%», а отримуєте 12%. Kotlin тут не винен: він зробив рівно те, що ви попросили. Якщо потрібне математичне заокруглення — спочатку заокруглюйте, потім приводьте до Int.
Помилка №3: робити Double → Int «десь потім», не думаючи про сенс.
Іноді пишуть: «Поки нехай буде Double, а в кінці перетворю на Int». Це небезпечно: ви можете втратити дані в момент, коли вже запізно зрозуміти, звідки взялася помилка. Краще мати чітку домовленість: де зберігаємо точне обчислення (Double), а де саме нам потрібен цілий результат (Int), і робити перетворення в зрозумілому місці.
Помилка №4: перетворювати цифру з Char у Int без перевірки діапазону.
Вираз ch.code - '0'.code коректний лише тоді, коли ch справді в діапазоні '0'..'9'. Якщо там опиниться 'A', ви отримаєте якесь число (наприклад, 17), і програма не впаде — вона просто почне робити нісенітницю. Тому спочатку перевірка, потім перетворення.
Помилка №5: забувати, що Char — це Unicode-символ, а не «літера з ASCII-таблиці».
У навчальних прикладах ми спокійно використовуємо 'A'..'Z' і '0'..'9', і це працює передбачувано. Але важливо памʼятати, що Char у Kotlin — це Unicode, і «літери» бувають не лише латинські. Саме тому коректніше мислити в термінах «код символу» й конкретних діапазонів, ніж у термінах «ну це ж літера, отже плюс один буде наступна».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ