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, и “буквы” бывают не только латинские. Именно поэтому корректнее мыслить в терминах “код символа” и конкретных диапазонов, чем в терминах “ну это же буква, значит плюс один будет следующая”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ