9.2 KiB
title, description, tags
| title | description | tags | |||
|---|---|---|---|---|---|
| Умные приведения | Smart cast после is и проверки на null, условия работы и случаи, когда компилятор отказывается |
|
Умное приведение (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 }
Далее: Платформенные типы