242 lines
9.2 KiB
Markdown
242 lines
9.2 KiB
Markdown
---
|
||
title: Умные приведения
|
||
description: Smart cast после is и проверки на null, условия работы и случаи, когда компилятор отказывается
|
||
tags:
|
||
- kotlin
|
||
- null-safety
|
||
- types
|
||
---
|
||
|
||
Умное приведение (smart cast) - автоматическое приведение типа после проверки. Компилятор отслеживает поток управления и знает, что в этой ветке тип уже уточнён.
|
||
|
||
```kotlin
|
||
fun describe(value: Any): String {
|
||
if (value is String) {
|
||
return "строка длины ${value.length}" // value уже String
|
||
}
|
||
return "не строка"
|
||
}
|
||
```
|
||
Явного `(String) value` писать не нужно - в отличие от Java (до версии 16).
|
||
|
||
## После проверки типа - is
|
||
```kotlin
|
||
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` работает так же, с инверсией:
|
||
```kotlin
|
||
fun check(x: Any) {
|
||
if (x !is String) return
|
||
println(x.length) // после return здесь только String
|
||
}
|
||
```
|
||
|
||
## После проверки на null
|
||
```kotlin
|
||
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
|
||
}
|
||
```
|
||
|
||
## В логических выражениях
|
||
Приведение работает внутри `&&` и `||`, так как они ленивые:
|
||
```kotlin
|
||
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 и безопасных вызовах
|
||
```kotlin
|
||
fun process(s: String?) {
|
||
val text = s ?: return // после этой строки s... нет, но text: String
|
||
println(text.length)
|
||
}
|
||
```
|
||
|
||
## Где приведение НЕ работает
|
||
Главное условие: компилятор должен гарантировать, что **значение не изменится между проверкой и использованием**.
|
||
|
||
### Изменяемое свойство класса - var
|
||
```kotlin
|
||
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`.
|
||
|
||
### Свойство с кастомным геттером
|
||
```kotlin
|
||
class Random2 {
|
||
val value: String?
|
||
get() = if (Math.random() > 0.5) "x" else null
|
||
|
||
fun broken() {
|
||
if (value != null) {
|
||
// println(value.length) // ошибка: геттер вернёт разное
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### open-свойство
|
||
```kotlin
|
||
open class Base {
|
||
open val value: String? = "x"
|
||
}
|
||
|
||
fun check(b: Base) {
|
||
if (b.value != null) {
|
||
// println(b.value.length) // ошибка: наследник мог переопределить геттер
|
||
}
|
||
}
|
||
```
|
||
|
||
### Свойство из другого модуля
|
||
Даже `val` из другой единицы компиляции не приводится: компилятор не может доказать, что у него нет кастомного геттера.
|
||
|
||
### Локальная var, захваченная лямбдой
|
||
```kotlin
|
||
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
|
||
Когда умное не применимо:
|
||
```kotlin
|
||
val x: Any = "text"
|
||
val s = x as String // ClassCastException при несовпадении
|
||
val n = x as? Int // null при несовпадении
|
||
```
|
||
Подробнее: [Приведение типов](../Работа%20с%20данными/Приведение%20типов.md)
|
||
|
||
## Умное приведение в Kotlin 2.x
|
||
Компилятор K2 (по умолчанию с Kotlin 2.0) расширил анализ. Теперь работает то, что раньше требовало ручного приведения:
|
||
```kotlin
|
||
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](../../Инструментарий/Компилятор%20K2.md)
|
||
|
||
> [!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](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
|
||
|
||
> [!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` - плохой обмен.** Оба компилируются, но второй вариант предсказуем:
|
||
> ```kotlin
|
||
> // println(value!!.length) // падение при гонке
|
||
> val v = value ?: return // гонки нет
|
||
> ```
|
||
>
|
||
> **Приведение дженериков неполное из-за стирания типов:**
|
||
> ```kotlin
|
||
> 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](../../Дженерики/Reified%20и%20inline.md).
|
||
>
|
||
> **Приведение теряется после вызова функции, меняющей состояние** - если это свойство, а не локальная переменная:
|
||
> ```kotlin
|
||
> class C {
|
||
> var s: String? = "x"
|
||
> fun reset() { s = null }
|
||
> }
|
||
> ```
|
||
>
|
||
> **В `when` без аргумента приведение работает только для проверенной переменной:**
|
||
> ```kotlin
|
||
> fun combine(a: Any?, b: Any?): Int = when {
|
||
> a is String && b is Int -> a.length + b // оба приведены
|
||
> else -> 0
|
||
> }
|
||
> ```
|
||
|
||
Далее: [Платформенные типы](Платформенные%20типы.md)
|