Приведение и проверка типов
Операторы is и !is
Используйте оператор is
или его отрицание !is
, чтобы проверить соответствует ли объект заданному типу
во время исполнения.
if (obj is String) {
print(obj.length)
}
if (obj !is String) { // то же самое, что и !(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 автоматически преобразовывается в String
}
}
Компилятор достаточно умён для того, чтобы понимать, что приведения безопасны в случаях,
когда проверка на несоответствие типу (!is
) приводит к выходу из функции:
if (x !is String) return
print(x.length) // x автоматически преобразовывается в String
или в случаях, когда приводимая переменная находится справа от оператора &&
или ||
, а соответствующая проверка
(обычная или отрицательная) находится слева:
// x автоматически преобразовывается в String справа от `||`
if (x !is String || x.length == 0) return
// x автоматически преобразовывается в String справа от `&&`
if (x is String && x.length > 0) {
print(x.length) // x автоматически преобразовывается в String
}
Умные приведения работают вместе с when
-выражениями
и циклами while
:
when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}
Заметьте, что умные приведения работают только тогда, когда компилятор может гарантировать, что переменная не изменится между проверкой и использованием. Точнее говоря, умные приведения будут работать:
- с локальными
val
переменными - всегда за исключением локальных делегированных свойств.; - с
val
свойствами - если поле имеет модификатор доступаprivate
илиinternal
, или проверка происходит в том же модуле, в котором объявлено это свойство. Умные приведения неприменимы к публичным свойствам или свойствам, которые имеют переопределённые getter’ы; - с локальными
var
переменными - если переменная не изменяется между проверкой и использованием, не захватывается лямбдой, которая её модифицирует и не является локальным делегированным свойством; - с
var
свойствами - никогда, потому что переменная может быть изменена в любое время другим кодом.
Оператор “небезопасного” приведения
Обычно оператор приведения выбрасывает исключение, если приведение невозможно, поэтому мы называем его небезопасным.
Небезопасное приведение в Kotlin выполняется с помощью инфиксного оператора as
:
val x: String = y as String
Заметьте, что null
не может быть приведен к String
, так как String
не является nullable,
т.е. если y
- null, код выше выбросит исключение. Чтобы сделать этот код корректным для null-значений,
используйте nullable-тип в правой части приведения.
val x: String? = y as String?
Оператор “безопасного” (nullable) приведения
Чтобы избежать исключения, вы можете использовать оператор безопасного приведения as?
, который возвращает null
в
случае неудачи.
val x: String? = y as? String
Заметьте, что несмотря на то, что справа от as?
стоит non-null тип String
, результат приведения является nullable.
Стирание и проверка типов у Обобщений (Generics)
Котлин обеспечивает типобезопасность операций, связанных с обобщениями на этапе компиляции,
в то время как информация о типе аргумента обобщения недоступна во время выполнения программы.
Например, для List<Foo>
происходит стирание типа, что превращает его в List<*>
. В связи с чем,
нет способа проверить, принадлежит ли объект конкретному типу во время выполнения программы.
Учитывая это, компилятор запрещает is
-проверки, которые не могут быть выполнены во время выполнения программы
из-за стирания типов, например ints is List<Int>
или list is T
(параметризированный тип).
Однако у вас есть возможность произвести проверку со “звёздными” проекциями.
if (something is List<*>) {
something.forEach { println(it) } // Элементы типа `Any?`
}
Таким же образом, когда у вас есть статически определенный тип аргумента, вы можете произвести is
-проверку
или приведение с необобщенной частью типа. Заметьте, что в данном случае угловые скобки пропущены:
fun handleStrings(list: List<String>) {
if (list is ArrayList) {
// `list` приводится к `ArrayList<String>` путём "умного приведения"
}
}
Аналогичный синтаксис, но с пропущенным типом аргумента может использоваться для приведений,
которые не принимают типы аргументы: list as ArrayList
.
Встроенные (inline) функции с параметрами вещественного типа имеют свои
аргументы типа, встроенные на каждый момент вызова, что позволяет arg is T
проверять параметризованный тип, но если
arg
является объектом обобщенного типа, его аргумент типа по-прежнему стираются.
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>>() // Нарушает типобезопасность!
fun main() {
println("stringToSomething = " + stringToSomething)
println("stringToInt = " + stringToInt)
println("stringToList = " + stringToList)
println("stringToStringList = " + stringToStringList)
//println(stringToStringList?.second?.forEach() {it.length}) // Это вызовет исключение ClassCastException, так как элементы списка не являются строками
}
Непроверяемые (Unchecked) приведения
Как упоминалось выше, стирание типов делает невозможным проверку типа аргумента обобщения на этапе выполнения, и обобщенные типы в коде могут быть недостаточно связаны друг с другом, чтобы компилятор обеспечил типобезопасность.
Тем не менее иногда мы имеем программную логику высокого уровня, которая подразумевает типобезопасность.
fun readDictionary(file: File): Map<String, *> = file.inputStream().use {
TODO("Прочитать сопоставление строк с произвольными элементами.")
}
// Мы сохранили словарь (map) `Int`ов в файл
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>
Компилятор выдает предупреждение для приведения в последней строке. Приведение не может быть полностью проверено во
время выполнения и нет дает гарантии, что значения в словаре (map) являются Int
.
Чтобы избежать непроверяемые приведения, вы можете изменить структуру программы: в примере выше
возможно объявить интерфейсы DictionaryReader<T>
и DictionaryWriter<T>
с типобезопасными имплементациями для различных типов. Правильное использование
вариативности обобщений также может помочь.
Для обобщенных функций, используемых встроенные (inline) функции с
параметрами вещественного типа приведение типа 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
В JVM, массивы (
Array<Foo>
) сохраняют информацию о стираемом типе их элементов, и приведение типов к массиву частично проверяется: nullability и фактические аргументы для параметризированных элементов массива все еще стираются. Например, приведениеfoo as Array <List <String>?>
будет успешным, еслиfoo
является массивомList <*>
, независимо от того, является ли он nullable или нет.