Files
obsidian/Kotlin/Синтаксис и типы/Работа с данными/Приведение типов.md
T

11 KiB
Raw Blame History

title, description, tags
title description tags
Приведение типов Проверка is, приведение as и as?, числовые преобразования и стирание типов
kotlin
types

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

fun describe(x: Any): String = when {
    x is String -> "строка длины ${x.length}"
    x is Int -> "число ${x + 1}"
    x !is Boolean -> "не логическое"
    else -> "логическое"
}

fun main() {
    println(describe("ab"))    // строка длины 2
    println(describe(5))       // число 6
    println(describe(1.0))     // не логическое
}

После успешной проверки компилятор сам приводит тип - это умное приведение, явный каст не нужен.

Небезопасное приведение - as

fun main() {
    val x: Any = "text"
    val s = x as String       // ok
    println(s.length)         // 4

    val n: Any = 42
    // val bad = n as String  // ClassCastException
}

as используют, когда тип известен точно, а компилятор об этом знать не может.

Безопасное приведение - as?

Возвращает null вместо исключения:

fun main() {
    val x: Any = "text"
    println(x as? Int)             // null
    println(x as? String)          // text
    println((x as? Int) ?: 0)      // 0 — частая связка с elvis
}

Результат всегда nullable, даже при приведении к non-null типу.

Сравнение способов:

Запись При несовпадении типа
x is T false
x as T ClassCastException
x as? T null

Приведение к nullable

fun main() {
    val a: Any? = null
    println(a as? String)       // null
    // println(a as String)     // NPE: null cannot be cast to non-null type
    println(a as String?)       // null — приведение к nullable разрешено
}

Числовые преобразования

Для чисел as не применяется - только явные функции:

fun main() {
    val i = 42
    println(i.toLong())      // 42
    println(i.toDouble())    // 42.0
    println(i.toByte())      // 42
    println(i.toChar())      // *
    println(3.99.toInt())    // 3 — обрезание, не округление
    println("42".toInt())    // 42
}

Неявных преобразований между числовыми типами нет:

fun main() {
    val i: Int = 42
    // val l: Long = i       // ошибка компиляции
    val l: Long = i.toLong() // так
}

Потеря данных происходит молча:

fun main() {
    println(300.toByte())            // 44 — старшие биты отброшены
    println(1e20.toInt())            // 2147483647 — обрезано
    println(Double.NaN.toInt())      // 0
}

Подробнее: Числа и переполнение

Стирание типов и дженерики

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

fun check(x: Any) {
    if (x is List<*>) {              // можно: звёздочка вместо типа
        println("список из ${x.size}")
    }
    // if (x is List<String>)        // ошибка: cannot check for instance
    //                               // of erased type
}

fun main() { check(listOf(1, 2)) }   // список из 2

List<*> - это star-проекция: «список чего-то неизвестного». Элементы из него читаются как Any?.

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

@Suppress("UNCHECKED_CAST")
fun <T> castList(x: Any): List<T> = x as List<T>   // unchecked cast

fun main() {
    val nums: List<Int> = castList(listOf("a", "b"))   // не упадёт здесь!
    // println(nums.first() + 1)    // ClassCastException только при обращении
    println(nums.size)              // 2 — работает, типы не проверяются
}

Это отложенное падение: ошибка проявится далеко от места приведения.

Обойти стирание можно через reified в inline-функциях:

inline fun <reified T> Any.safeCast(): T? = this as? T

fun main() {
    println("text".safeCast<String>())   // text
    println("text".safeCast<Int>())      // null
}

Подробнее: Reified и inline

Приведение в иерархии

open class Animal(val name: String)
class Dog(name: String) : Animal(name) {
    fun bark() = "Гав"
}

fun main() {
    val a: Animal = Dog("Рекс")       // вверх по иерархии — неявно
    println(a.name)                    // Рекс

    val d = a as Dog                   // вниз — только явно
    println(d.bark())                  // Гав

    val other: Animal = Animal("кто-то")
    println((other as? Dog)?.bark())   // null — безопасно
}

Приведение вверх (к супертипу) всегда безопасно и неявно. Приведение вниз требует as или as?.

Коллекции и вариантность

fun main() {
    val strings: List<String> = listOf("a")
    val anys: List<Any> = strings        // ok — List ковариантен по out T
    println(anys.size)                    // 1

    val mStrings: MutableList<String> = mutableListOf("a")
    // val mAnys: MutableList<Any> = mStrings  // ошибка — MutableList инвариантен
}

List<out T> разрешает такое присваивание, MutableList<T> - нет, иначе можно было бы положить Int в список строк. Подробнее: Вариантность

Получение типа в рантайме

fun main() {
    val x: Any = "text"
    println(x::class)                  // class kotlin.String
    println(x::class.simpleName)       // String
    println(x::class.java)             // class java.lang.String — Java-класс
    println(x is CharSequence)         // true — проверка по интерфейсу
}

Подробнее: Рефлексия

[!info] Отличия от Java

Java Kotlin
x instanceof String x is String
(String) x x as String
нет безопасного каста x as? String
(int) doubleValue doubleValue.toInt()
long l = intValue; неявно intValue.toLong() явно
x.getClass() x::class или x::class.java
после instanceof нужен каст умное приведение
массивы ковариантны массивы инвариантны

Главные приобретения: as? вместо пары instanceof + каст, и запрет неявных числовых преобразований, из-за которых в Java long total = intA * intB тихо переполняется.

Ковариантность массивов в Java - известная дыра в типизации:

Object[] objs = new String[1];
objs[0] = 42;        // компилируется, падает в рантайме

В Kotlin Array<T> инвариантен, и такой код не соберётся.

Сравнить: Приведение ссылочных типов в Java

[!warning] Подводные камни as на null даёт NPE, а не ClassCastException:

fun main() {
    val a: Any? = null
    // println(a as String)    // NullPointerException
    println(a as String?)      // null — ok
}

as? маскирует обе причины. null в результате означает либо неверный тип, либо что значение изначально было null - различить невозможно:

fun main() {
    val a: Any? = null
    val b: Any = 42
    println("${a as? String} ${b as? String}")   // null null
}

Unchecked cast падает не там, где написан:

@Suppress("UNCHECKED_CAST")
fun main() {
    val list = listOf("a") as List<Int>   // здесь не падает
    println(list.size)                     // 1 — и здесь тоже
    // println(list[0] + 1)                // ClassCastException только тут
}

Поэтому @Suppress("UNCHECKED_CAST") - всегда повод задуматься.

toInt() на Double обрезает дробную часть, а не округляет. 3.99.toInt() даёт 3. Для округления - roundToInt().

toByte() и toShort() молча теряют старшие биты: 300.toByte() возвращает 44.

is не проверяет параметры дженерика. x is List<String> не скомпилируется, а x is List<*> ничего не говорит об элементах. Проверять придётся вручную:

fun main() {
    val x: Any = listOf(1, "a")
    val strings = (x as? List<*>)?.filterIsInstance<String>()
    println(strings)      // [a] — только элементы нужного типа
}

x::class и x::class.java - разные вещи. Первое - KClass Kotlin, второе - Class Java. Для Int они могут отличаться (int против Integer).

Дополнительно: Умные приведения Далее: Any, Unit, Nothing