Files
obsidian/Kotlin/Синтаксис и типы/Null-безопасность/Умные приведения.md
T

242 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)