--- title: Умные приведения description: Smart cast после is и проверки на null, условия работы и случаи, когда компилятор отказывается tags: - kotlin - null-safety - types --- Умное приведение (smart cast) - автоматическое приведение типа после проверки. Компилятор отслеживает поток управления и знает, что в этой ветке тип уже уточнён. ```kotlin fun describe(value: Any): String { if (value is String) { return "строка длины ${value.length}" // value уже String } return "не строка" } ``` Явного `(String) value` писать не нужно - в отличие от Java (до версии 16). ## После проверки типа - is ```kotlin fun size(x: Any): Int = when (x) { is String -> x.length // String is List<*> -> x.size // List<*> is Map<*, *> -> x.size // Map<*, *> else -> 0 } ``` `!is` работает так же, с инверсией: ```kotlin fun check(x: Any) { if (x !is String) return println(x.length) // после return здесь только String } ``` ## После проверки на null ```kotlin fun length(s: String?): Int { if (s == null) return 0 return s.length // s: String } fun length2(s: String?): Int { if (s != null) { return s.length // s: String } return 0 } ``` ## В логических выражениях Приведение работает внутри `&&` и `||`, так как они ленивые: ```kotlin fun check(x: Any?): Boolean { return x is String && x.length > 3 // x приведён во второй части } fun check2(x: Any?): Boolean { return x !is String || x.length > 3 // приведён в правой части || } ``` ## В elvis и безопасных вызовах ```kotlin fun process(s: String?) { val text = s ?: return // после этой строки s... нет, но text: String println(text.length) } ``` ## Где приведение НЕ работает Главное условие: компилятор должен гарантировать, что **значение не изменится между проверкой и использованием**. ### Изменяемое свойство класса - var ```kotlin class Holder { var value: String? = null fun broken() { if (value != null) { // println(value.length) // ошибка: smart cast to 'String' is impossible } } fun fixed() { val local = value // копия в локальную val if (local != null) { println(local.length) // работает } } } ``` Почему: между проверкой и обращением другой поток (или вызванная функция) может присвоить `value = null`. ### Свойство с кастомным геттером ```kotlin class Random2 { val value: String? get() = if (Math.random() > 0.5) "x" else null fun broken() { if (value != null) { // println(value.length) // ошибка: геттер вернёт разное } } } ``` ### open-свойство ```kotlin open class Base { open val value: String? = "x" } fun check(b: Base) { if (b.value != null) { // println(b.value.length) // ошибка: наследник мог переопределить геттер } } ``` ### Свойство из другого модуля Даже `val` из другой единицы компиляции не приводится: компилятор не может доказать, что у него нет кастомного геттера. ### Локальная var, захваченная лямбдой ```kotlin fun broken() { var s: String? = "text" val modify = { s = null } if (s != null) { // println(s.length) // ошибка: лямбда может изменить s } } ``` ## Сводка условий | Что проверяем | Умное приведение | |---|---| | локальная `val` | ✅ работает | | локальная `var` без захвата лямбдой | ✅ работает | | локальная `var`, захваченная лямбдой | ❌ | | `val`-свойство в том же модуле, без геттера | ✅ работает | | `var`-свойство | ❌ | | `open val` | ❌ | | `val` с кастомным геттером | ❌ | | `val` из другого модуля | ❌ | | параметр функции (всегда `val`) | ✅ работает | Универсальное решение во всех ❌-случаях: скопировать значение в локальную `val`. ## Явное приведение - as Когда умное не применимо: ```kotlin val x: Any = "text" val s = x as String // ClassCastException при несовпадении val n = x as? Int // null при несовпадении ``` Подробнее: [Приведение типов](../Работа%20с%20данными/Приведение%20типов.md) ## Умное приведение в Kotlin 2.x Компилятор K2 (по умолчанию с Kotlin 2.0) расширил анализ. Теперь работает то, что раньше требовало ручного приведения: ```kotlin fun test(x: Any?) { val isString = x is String if (isString) { println(x.length) // K2 понимает связь через переменную-флаг } } fun orElse(x: Any?) { if (x !is String) return println(x.length) // работало и раньше } ``` Подробнее: [Компилятор K2](../../Инструментарий/Компилятор%20K2.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `if (x instanceof String) { String s = (String) x; ... }` | `if (x is String) { x.length }` | > | pattern matching `x instanceof String s` (Java 16+) | есть с первой версии, без объявления переменной | > | приведение только по типу | ещё и по null | > | компилятор не отслеживает изменяемость | отказывается приводить, если значение может измениться | > > Java 16 добавила pattern matching: `if (x instanceof String s)`. Это ближе к Kotlin, но Java всё равно требует явно назвать переменную, и на nullability это не распространяется. > > Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md) > [!warning] Подводные камни > **Сообщение компилятора объясняет причину** - его стоит читать целиком: > ``` > smart cast to 'String' is impossible, because 'value' is a mutable property > that could be mutated concurrently > > smart cast to 'String' is impossible, because 'value' is a property > that has an open or custom getter > > smart cast to 'String' is impossible, because 's' is a local variable > that is mutated in a capturing closure > ``` > Там прямо сказано и что мешает, и почему. > > **`!!` вместо локальной `val` - плохой обмен.** Оба компилируются, но второй вариант предсказуем: > ```kotlin > // println(value!!.length) // падение при гонке > val v = value ?: return // гонки нет > ``` > > **Приведение дженериков неполное из-за стирания типов:** > ```kotlin > fun check(x: Any) { > if (x is List<*>) { > // элементы остаются Any?, тип параметра неизвестен > val first = x.firstOrNull() // Any? > } > // if (x is List) // ошибка: cannot check for instance of erased type > } > ``` > Обход - [reified](../../Дженерики/Reified%20и%20inline.md). > > **Приведение теряется после вызова функции, меняющей состояние** - если это свойство, а не локальная переменная: > ```kotlin > class C { > var s: String? = "x" > fun reset() { s = null } > } > ``` > > **В `when` без аргумента приведение работает только для проверенной переменной:** > ```kotlin > fun combine(a: Any?, b: Any?): Int = when { > a is String && b is Int -> a.length + b // оба приведены > else -> 0 > } > ``` Далее: [Платформенные типы](Платформенные%20типы.md)