1. Вступ
Коли ви лише починаєте програмувати, здається логічним: «Ну я ж написав val x: String, отже, це рядок — і все». У більшості випадків так і є: Kotlin робить усе, щоб ви могли працювати спокійно. Але щойно ви починаєте писати функції, які приймають загальніший тип (наприклад, Any або базовий клас), ви стикаєтеся з реальністю: у цю змінну можуть потрапляти різні варіанти даних. І тоді виникає питання: «Що саме там зараз лежить і чи можна викликати в цього обʼєкта методи?»
Уявіть наш консольний трекер витрат (умовний застосунок курсу): у ньому є «внутрішня» модель Expense, а введення користувача — рядок. Іноді ми хочемо написати допоміжну функцію, яка зможе прийняти «щось» (рядок, число, null) і спробувати перетворити це на суму. Або ж ми пишемо обробник результату операції, де повертається загальний тип, а всередині — різні варіанти результату. У таких місцях Kotlin просить нас бути чесними: спочатку перевірте тип — і лише потім використовуйте значення.
Any і Any?: найзагальніший тип
Якщо Kotlin — суворий учитель, то Any — це його фраза: «Гаразд, принеси що завгодно, але потім я запитаю, що саме ти приніс». Тип Any означає «будь-який ненульовий (non-null) обʼєкт». Це важливо: null не є значенням типу Any, тому що Any — ненульовий тип. Якщо хочете «що завгодно, включно з null», це буде Any?.
Any корисний, коли ви справді не знаєте точного типу наперед: наприклад, у колекції «різних» значень, в універсальному парсері, у функції логування, в адаптері над чужими даними. Але в Any є ціна: доки змінна має тип Any, ви не можете просто так викликати методи String або звернутися до .length. Спочатку потрібно довести компілятору, що це справді рядок (або інший конкретний тип).
Невеличка табличка, щоб закріпити:
| Тип | Що означає | Чи можна зберігати null |
|---|---|---|
|
будь-який ненульовий обʼєкт | ні |
|
будь-який обʼєкт або null | так |
2. Перевірка типу: is і !is
Оператор is — це ваш «детектор типу». Він перевіряє під час виконання, чи належить значення до вказаного типу. Якщо так — умова істинна, якщо ні — хибна. Є й !is, якщо зручніше читати перевірку як «не такого типу».
Найприємніше те, що is зазвичай запускає smart cast (розумне приведення). Тобто всередині гілки if Kotlin дозволяє звертатися до змінної як до конкретнішого типу — без явного as. Про це детальніше в наступному розділі, а поки — невеликий приклад.
fun printLength(x: Any) {
if (x is String) {
println(x.length) // наприклад: 5
} else {
println("Це не рядок")
}
}
fun main() {
printLength("Hello") // 5
printLength(42) // Це не рядок
}
Тут x зовні — Any, але всередині if (x is String) він стає «ніби String». Це і є smart cast у дії.
3. Smart cast: компілятор «сам зрозумів», якщо йому не заважати
Smart cast — це не магія, а акуратний аналіз потоку виконання. Компілятор дивиться на ваші перевірки (is, != null, деякі умови) і робить висновки: «у цій гілці значення точно не null» або «у цій гілці значення точно String». Після цього він дозволяє звертатися до нього як до конкретнішого типу без явного приведення.
У Kotlin із компілятором K2 (починаючи з Kotlin 2.0) smart cast став можливим у більшій кількості ситуацій, ніж раніше. Наприклад, коли ви винесли перевірку типу в окрему булеву змінну й використовуєте її в if.
Подивімося на приклад у стилі «нашого застосунку». Нехай у нас є команда, і ми тимчасово зберігаємо «аргумент» як Any (бо тести або різні джерела даних можуть підкинути різні типи).
fun normalizeAmount(raw: Any): Int? {
val isText = raw is String
if (isText) {
return raw.toIntOrNull()
}
return null
}
У Kotlin 2.x компілятор краще «бачить» звʼязок між isText і тим, що raw можна розглядати як String усередині гілки.
Коли smart cast не спрацьовує
Є ситуації, коли Kotlin чесно скаже: «Я не можу гарантувати, що тип не зміниться». Найчастіший випадок — коли ви перевіряєте змінюване значення, яке потенційно може змінитися між перевіркою та використанням. На рівні початкового курсу це можна сформулювати так: smart cast любить val і не дуже любить var.
Ще один частий сценарій: ви щось перевірили, але потім зробили дію, після якої значення могло змінитися (або компілятор не впевнений, що не змінилося). Тоді він може «скинути впевненість» і знову вимагати безпечний виклик або явне приведення. Це не шкідливість — це захист.
4. Небезпечне приведення: as
Іноді ви впевнені, що значення має потрібний тип, і хочете прямо сказати Kotlin: «Вважай це String». Для цього є оператор as.
Проблема в тому, що as — не перевірка, а спроба привести тип. Якщо тип не підходить, програма впаде під час виконання з ClassCastException. Тобто компілятор пропустить код, а помилка зʼявиться вже під час запуску.
Мініприклад (спеціально «небезпечний»):
fun forceString(x: Any): String {
return x as String
}
fun main() {
println(forceString("OK")) // OK
println(forceString(123)) // ClassCastException під час виконання
}
Чому це важливо саме зараз? Тому що as часто починають використовувати навмання: «ну, наче там рядок…». Так робити не варто. as доречний, коли у вас справді є жорстка гарантія типу (наприклад, ви самі поклали туди String і нічого іншого туди не кладете).
5. Безпечне приведення: as? і Elvis ?:
Оператор as? — це «мʼяка версія as». Він намагається привести тип, але якщо не виходить — повертає null замість падіння.
Це ідеально лягає на весь наш попередній досвід із nullable-типами: далі ви або робите перевірку на null, або використовуєте оператор Елвіса ?:.
Приклад:
fun stringOrNull(x: Any): String? = x as? String
fun main() {
println(stringOrNull("Hi")?.length) // 2
println(stringOrNull(123)?.length) // null
}
Ще практичніше — одразу задавати «значення за замовчуванням»:
fun stringLengthOrZero(x: Any): Int {
val s = x as? String ?: return 0
return s.length
}
fun main() {
println(stringLengthOrZero("Kotlin")) // 6
println(stringLengthOrZero(10)) // 0
}
Тут це читається прямолінійно: «спробуй привести до рядка, якщо не вийшло — 0».
6. Корисні прийоми: як не плутатися й писати компактно
Порівняння is, as, as?
Коли ці три інструменти зʼявляються поруч, мозок новачка інколи робить вигляд, що він — старий принтер: починає гудіти й видає порожній аркуш. Зафіксуймо відмінності максимально приземлено.
| Інструмент | Що робить | Що повертає | Якщо тип не підходить |
|---|---|---|---|
|
перевіряє тип | |
просто false |
|
приводить тип | |
падає з ClassCastException |
|
приводить тип безпечно | |
повертає null |
Якщо вам потрібно «просто зрозуміти, що там» — починайте з is. Якщо потрібно «акуратно спробувати взяти потрібний тип» — обирайте as?. as залишайте для випадків, де ви справді впевнені й готові відповідати за цю впевненість.
when за типами та smart cast
Іноді замість кількох if (x is …) зручніше писати when і перевіряти тип у гілках. Це особливо корисно, коли у вас є 3–4 очікувані форми даних і ви хочете акуратну «таблицю обробки».
fun describeValue(x: Any): String =
when (x) {
is String -> "Рядок завдовжки ${x.length}"
is Int -> "Ціле число $x"
else -> "Щось інше"
}
fun main() {
println(describeValue("Hi")) // Рядок завдовжки 2
println(describeValue(7)) // Ціле число 7
}
Усередині кожної гілки x отримує smart cast до потрібного типу, і ви спокійно використовуєте .length або друкуєте число. Це той самий принцип, що й у if, просто форма запису компактніша.
7. Приклад: парсимо суму витрати з Any?
У реальному коді рідко буває мета «просто привести тип заради приведення». Зазвичай це частина зрозумілішої задачі: розпарсити введення, витягнути поле зі структури, обробити різні формати даних.
Уявімо, що в нашому трекері витрат є функція, яка отримує «сире значення суми» з якогось джерела. У простому консольному варіанті це String із readln(), але інколи ми можемо викликати функцію з тесту й передати туди Int. Ми хочемо зробити універсальний обробник, який прийме Any? і спробує отримати суму в копійках (ціле число), а в іншому разі поверне null.
fun parseAmountCents(raw: Any?): Int? {
if (raw is Int) return raw
val text = raw as? String ?: return null
return text.trim().toIntOrNull()
}
fun main() {
println(parseAmountCents(" 120 ")) // 120
println(parseAmountCents(55)) // 55
println(parseAmountCents("oops")) // null
}
Зверніть увагу, як тут «усе по-дорослому, але без пафосу»: спочатку ми робимо короткий шлях — якщо це вже Int, чудово. Потім безпечно намагаємося отримати рядок через as?. Якщо рядка немає — null. Якщо рядок є — чистимо й парсимо.
Невеличка блок-схема
flowchart TD
A["raw: Any?"] --> B{raw is Int?}
B -- так --> C["повернути raw"]
B -- ні --> D{raw as? String}
D -- null --> E["повернути null"]
D -- String --> F["trim + toIntOrNull()"]
F --> G["повернути Int?"]
Така схема хороша тим, що показує: is — це розвилка за типом, а as? — «спроба дістати» потрібний тип без падіння.
8. Типові помилки під час роботи з is, smart cast, as, as?
Помилка №1: використовувати as як «перевірку типу».
Дуже поширена логіка новачка: «якщо as не спрацює — значить, не той тип». Але проблема в тому, що «не спрацює» тут означає падіння програми з ClassCastException. Якщо тип може бути різним — спочатку перевірка is або безпечне as?, а as залишаємо лише для випадків із реальною гарантією.
Помилка №2: забувати, що as? повертає nullable (T?).
as? не дає вам «чистий тип», він дає «можливо, тип». Тому код на кшталт val s = x as? String; println(s.length) не компілюється — і це добре. Правильне продовження: s?.length, if (s != null), або Elvis ?: для значення за замовчуванням.
Помилка №3: очікувати smart cast там, де компілятор не може дати гарантій.
Якщо значення може змінюватися (часто це повʼязано з var), компілятор може відмовитися робити smart cast, бо «між перевіркою та використанням» обʼєкт теоретично може стати іншим. Це не прискіпливість, а страховка. У таких місцях або переписують код так, щоб перевіряти й використовувати значення «поруч», або зберігають у локальну val і працюють із нею.
Помилка №4: надто рано узагальнювати все до Any.
Any здається зручним: «одна функція на все». Але в цього є ціна: майже одразу зʼявляються перевірки типів, when, as?, і код стає складніше читати. Хороше правило: використовуйте Any лише там, де це справді виправдано контрактом (наприклад, універсальний парсер/адаптер), а в інших місцях надавайте перевагу конкретним типам у параметрах функцій.
Помилка №5: змішувати «перевірку типу» і «бізнес-логіку» в одну кашу.
Коли всередині if (x is String) ви починаєте писати 40 рядків логіки, код швидко стає нечитабельним. Практичніше зробити невеликий блок, у якому ви акуратно приводите тип (або повертаєте помилку/null), а вже далі працювати з нормальним String/Int і писати зрозумілу предметну логіку без постійних перевірок.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ