Проверка типов и приведение типов: '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