Spec-Zone.ru › Kotlin 1.4

Проверка типов и приведение типов: 'is' и 'as'

is и !is операторы

Мы можем проверить, соответствует ли объект заданному типу во время выполнения, используя оператор is или его отрицательную форму !is.

if (obj is String) {
    print(obj.length)
}

if (obj !is String) { // same as !(obj is String)
    print("Not a String")
}
else {
    print(obj.length)
}

Умные приведения типов

Во многих случаях использование явных операторов приведения в Kotlin не требуется, так как компилятор отслеживает проверки is и явные приведения для неизменяемых значений и вставляет (безопасные) приведения автоматически при необходимости:

fun demo(x: Any) {
    if (x is String) {
        print(x.length) // x is automatically cast to String
    }
}

Компилятор достаточно умён, чтобы определить, является ли приведение безопасным, если отрицательная проверка приводит к возврату значения:

if (x !is String) return

print(x.length) // x is automatically cast to String

или в правой части && и ||:

// x is automatically cast to string on the right-hand side of `||`
if (x !is String || x.length == 0) return

// x is automatically cast to string on the right-hand side of `&&`
if (x is String && x.length > 0) {
    print(x.length) // x is automatically cast to String
}

Такие умные приведения работают и для when-выражений, и для while-циклов:

when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

Обратите внимание, что умные приведения не работают, когда компилятор не может гарантировать, что переменная не изменится между проверкой и использованием. Более конкретно, умные приведения применяются в соответствии со следующими правилами:

  • val локальные переменные — всегда, за исключением локальных делегированных свойств;
  • val свойства — если свойство является приватным или внутренним, или проверка выполняется в том же модуле, где объявлено свойство. Умные приведения не применяются к открытым свойствам или свойствам с пользовательскими геттерами;
  • var локальные переменные — если переменная не изменяется между проверкой и использованием, не захвачена в лямбда-выражение, которое изменяет её, и не является локальным делегированным свойством;
  • var свойства — никогда (потому что переменная может быть изменена в любое время другим кодом).

"Небезопасный" оператор приведения

Обычно оператор приведения выбрасывает исключение, если приведение невозможно. Поэтому мы называем его небезопасным. Небезопасное приведение в Kotlin выполняется с помощью инфиксного оператора as (см. приоритет операторов):

val x: String = y as String

Обратите внимание, что null нельзя привести к String, так как этот тип не является nullable, т. е. если y равно null, приведенный выше код выбросит исключение. Чтобы сделать такой код корректным для значений null, используйте nullable тип в правой части приведения:

val x: String? = y as String?

Обратите внимание, что оператор "небезопасного" приведения не эквивалентен методу unsafeCast<T>(), доступному в Kotlin/JS. unsafeCast вообще не будет выполнять проверку типов, в то время как оператор приведения выбрасывает ClassCastException при неудачном приведении.

"Безопасный" (nullable) оператор приведения

Чтобы избежать выброса исключения, можно использовать безопасный оператор приведения as?, который возвращает null при неудаче:

val x: String? = y as? String

Обратите внимание, что, несмотря на то, что правая часть оператора as? является типом без null String, результат приведения является nullable.

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

Kotlin обеспечивает безопасность типов операций, связанных с дженериками, на этапе компиляции, а во время выполнения экземпляры дженерических типов не содержат информации о фактических аргументах типа. Например, List<Foo> стирается до простого List<*>. В общем случае нет способа проверить, принадлежит ли экземпляр дженерическому типу с определёнными аргументами типа во время выполнения.

Учитывая это, компилятор запрещает проверки is, которые не могут быть выполнены во время выполнения из-за стирания типов, такие как ints is List<Int> или list is T (параметр типа). Однако вы можете проверить экземпляр на соответствие звёздному проекции типа:

if (something is List<*>) {
    something.forEach { println(it) } // The items are typed as `Any?`
}

Аналогично, когда у вас уже есть аргументы типа экземпляра, проверенные статически (на этапе компиляции), вы можете выполнить проверку is или приведение, которое включает недженерическую часть типа. Обратите внимание, что угловые скобки в этом случае опущены:

fun handleStrings(list: List<String>) {
    if (list is ArrayList) {
        // `list` is smart-cast to `ArrayList<String>`
    }
}

Такую же синтаксическую конструкцию с опущенными аргументами типа можно использовать для приведений, которые не учитывают аргументы типа: list as ArrayList.

Вложенные функции с реализованными параметрами типа имеют фактические аргументы типа, встроенные в каждой точке вызова, что позволяет выполнять проверки arg is T для параметров типа, но если arg является экземпляром дженерического типа, его аргументы типа по-прежнему стираются. Пример:

//sampleStart
inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}

val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)

val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
val stringToList = somePair.asPairOf<String, List<*>>()
val stringToStringList = somePair.asPairOf<String, List<String>>() // Breaks type safety!
//sampleEnd

fun main() {
    println("stringToSomething = " + stringToSomething)
    println("stringToInt = " + stringToInt)
    println("stringToList = " + stringToList)
    println("stringToStringList = " + stringToStringList)
}

Непроверенные приведения типов

Как сказано выше, стирание типов делает невозможной проверку фактических аргументов типа экземпляра дженерического типа во время выполнения, и дженерические типы в коде могут быть связаны друг с другом недостаточно тесно, чтобы компилятор мог гарантировать безопасность типов.

Тем не менее, иногда у нас есть логика программы высокого уровня, которая подразумевает безопасность типов. Например:

fun readDictionary(file: File): Map<String, *> = file.inputStream().use { 
    TODO("Read a mapping of strings to arbitrary elements.")
}

// We saved a map with `Int`s into that file
val intsFile = File("ints.dictionary")

// Warning: Unchecked cast: `Map<String, *>` to `Map<String, Int>`
val intsDictionary: Map<String, Int> = readDictionary(intsFile) as Map<String, Int>

Компилятор выводит предупреждение для приведения в последней строке. Приведение не может быть полностью проверено во время выполнения и не гарантирует, что значения в карте являются Int.

Чтобы избежать непроверенных приведений, можно переработать структуру программы: в приведённом выше примере могут быть интерфейсы DictionaryReader<T> и DictionaryWriter<T> с безопасными типами реализации для различных типов. Можно ввести разумные абстракции, чтобы перенести непроверенные приведения из вызывающего кода в детали реализации. Правильное использование вариативности дженериков также может помочь.

Для дженерических функций использование реализованных параметров типа делает приведения, такие как arg as T проверенными, если только тип arg не имеет своих аргументов типа, которые стираются.

Предупреждение о непроверенном приведении можно подавить, добавив аннотацию к предложению или объявлению, где оно встречается, с @Suppress("UNCHECKED_CAST"):

inline fun <reified T> List<*>.asListOfType(): List<T>? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST")
        this as List<T> else
        null

IntelliJ IDEA также может автоматически генерировать аннотацию @Suppress . Откройте меню намерений через значок лампочки или Alt-Enter и щелкните стрелку рядом с быстрой исправлением «Изменить аргументы типов». Здесь вы можете выбрать область подавления, и ваш IDE добавит аннотацию в файл соответственно.

В JVM, типы массивов (Array<Foo>) сохраняют информацию о стёртом типе их элементов, и приведения типов к типу массива частично проверяются: nullable и фактические аргументы типа элементов всё ещё стираются. Например, приведение foo as Array<List<String>?> будет успешным, если foo представляет собой массив, содержащий любые List<*>, nullable или нет.

© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/typecasts.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API