Files
obsidian/Kotlin/Синтаксис и типы/Null-безопасность/Умные приведения.md
T

9.2 KiB
Raw Blame History

title, description, tags
title description tags
Умные приведения Smart cast после is и проверки на null, условия работы и случаи, когда компилятор отказывается
kotlin
null-safety
types

Умное приведение (smart cast) - автоматическое приведение типа после проверки. Компилятор отслеживает поток управления и знает, что в этой ветке тип уже уточнён.

fun describe(value: Any): String {
    if (value is String) {
        return "строка длины ${value.length}"   // value уже String
    }
    return "не строка"
}

Явного (String) value писать не нужно - в отличие от Java (до версии 16).

После проверки типа - is

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 работает так же, с инверсией:

fun check(x: Any) {
    if (x !is String) return
    println(x.length)            // после return здесь только String
}

После проверки на null

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
}

В логических выражениях

Приведение работает внутри && и ||, так как они ленивые:

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 и безопасных вызовах

fun process(s: String?) {
    val text = s ?: return       // после этой строки s... нет, но text: String
    println(text.length)
}

Где приведение НЕ работает

Главное условие: компилятор должен гарантировать, что значение не изменится между проверкой и использованием.

Изменяемое свойство класса - var

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.

Свойство с кастомным геттером

class Random2 {
    val value: String?
        get() = if (Math.random() > 0.5) "x" else null

    fun broken() {
        if (value != null) {
            // println(value.length)   // ошибка: геттер вернёт разное
        }
    }
}

open-свойство

open class Base {
    open val value: String? = "x"
}

fun check(b: Base) {
    if (b.value != null) {
        // println(b.value.length)   // ошибка: наследник мог переопределить геттер
    }
}

Свойство из другого модуля

Даже val из другой единицы компиляции не приводится: компилятор не может доказать, что у него нет кастомного геттера.

Локальная var, захваченная лямбдой

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

Когда умное не применимо:

val x: Any = "text"
val s = x as String          // ClassCastException при несовпадении
val n = x as? Int            // null при несовпадении

Подробнее: Приведение типов

Умное приведение в Kotlin 2.x

Компилятор K2 (по умолчанию с Kotlin 2.0) расширил анализ. Теперь работает то, что раньше требовало ручного приведения:

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

[!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

[!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 - плохой обмен. Оба компилируются, но второй вариант предсказуем:

// println(value!!.length)       // падение при гонке
val v = value ?: return          // гонки нет

Приведение дженериков неполное из-за стирания типов:

fun check(x: Any) {
    if (x is List<*>) {
        // элементы остаются Any?, тип параметра неизвестен
        val first = x.firstOrNull()   // Any?
    }
    // if (x is List<String>)   // ошибка: cannot check for instance of erased type
}

Обход - reified.

Приведение теряется после вызова функции, меняющей состояние - если это свойство, а не локальная переменная:

class C {
    var s: String? = "x"
    fun reset() { s = null }
}

В when без аргумента приведение работает только для проверенной переменной:

fun combine(a: Any?, b: Any?): Int = when {
    a is String && b is Int -> a.length + b   // оба приведены
    else -> 0
}

Далее: Платформенные типы