Files
obsidian/Kotlin/Синтаксис и типы/Базовые типы ООП/Value class.md
T

266 lines
11 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: Value class
description: Обёртка без аллокации, типобезопасность примитивов, mangling имён и границы инлайнинга
tags:
- kotlin
- oop
- performance
---
`value class` - обёртка над одним значением, которая в байт-коде обычно **исчезает**: вместо объекта остаётся само значение.
```kotlin
@JvmInline
value class UserId(val value: Int)
fun load(id: UserId) = "пользователь ${id.value}"
fun main() {
val id = UserId(42)
println(load(id)) // пользователь 42
println(id.value) // 42
}
```
В скомпилированном коде `load` принимает `int`, а не объект. Аллокации нет.
Аннотация `@JvmInline` обязательна на JVM - без неё компилятор выдаст ошибку.
## Зачем
Основная причина - **типобезопасность без накладных расходов**. Классическая проблема:
```kotlin
fun transfer(from: Int, to: Int, amount: Int) = "$from -> $to: $amount"
fun main() {
println(transfer(100, 1, 2)) // перепутали порядок — компилятор молчит
}
```
Все параметры `Int`, поэтому их легко перепутать. С `value class` такая ошибка становится ошибкой компиляции:
```kotlin
@JvmInline value class AccountId(val value: Int)
@JvmInline value class Amount(val cents: Int)
fun transfer(from: AccountId, to: AccountId, amount: Amount) =
"${from.value} -> ${to.value}: ${amount.cents}"
fun main() {
println(transfer(AccountId(1), AccountId(2), Amount(100)))
// println(transfer(Amount(100), AccountId(1), AccountId(2))) // не скомпилируется
}
```
Цена - ноль: в байт-коде всё те же три `int`.
## Что можно внутри
```kotlin
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "неверный email: $value" }
}
val domain: String get() = value.substringAfter("@")
fun masked() = value.replaceRange(0, value.indexOf("@"), "***")
companion object {
fun parse(s: String): Email? = if (s.contains("@")) Email(s) else null
}
}
fun main() {
val e = Email("ivan@example.com")
println(e.domain) // example.com
println(e.masked()) // ***@example.com
println(Email.parse("нет")) // null
}
```
Разрешены: `init`, свойства с геттером, функции, `companion object`, реализация интерфейсов.
Запрещены: второе свойство с полем, `var`-свойства с полем, наследование, блоки инициализации полей, `lateinit`.
```kotlin
// @JvmInline value class Bad(val a: Int, val b: Int) // ошибка: только одно поле
// @JvmInline value class Bad2(var a: Int) // ошибка: нужен val
```
## Реализация интерфейса
```kotlin
interface Printable { fun print2(): String }
@JvmInline
value class Code(val value: Int) : Printable {
override fun print2() = "код $value"
}
fun main() {
println(Code(7).print2()) // код 7
val p: Printable = Code(7) // упаковка! см. ниже
println(p.print2()) // код 7
}
```
## Когда обёртка всё-таки создаётся
Инлайнинг не бесплатен в смысле предсказуемости: в некоторых позициях объект приходится создать.
| Позиция | Инлайнится |
|---|---|
| параметр и результат функции | да |
| локальная переменная | да |
| nullable-тип (`UserId?`) | **нет** |
| приведение к супертипу/интерфейсу | **нет** |
| элемент коллекции (`List<UserId>`) | **нет** - дженерики стираются |
| поле дженерик-класса | **нет** |
```kotlin
@JvmInline value class UserId(val value: Int)
fun main() {
val a: UserId = UserId(1) // int, без объекта
val b: UserId? = UserId(1) // объект UserId
val c: List<UserId> = listOf(UserId(1)) // объекты внутри списка
println("${a.value} ${b?.value} ${c.size}")
}
```
То есть `List<UserId>` по памяти не лучше `List<Integer>`. Выгода - в типобезопасности, а не в аллокациях.
## Mangling имён
Чтобы разные `value class` над одним типом не давали одинаковые JVM-сигнатуры, компилятор добавляет суффикс к имени метода:
```kotlin
@JvmInline value class AccountId(val value: Int)
fun process(id: AccountId) = id.value // в байт-коде: process-<хеш>(I)I
fun main() { println(process(AccountId(1))) } // 1
```
Из Java такую функцию вызвать практически нельзя - имя непредсказуемо. Решение - `@JvmName`:
```kotlin
@JvmInline value class AccountId(val value: Int)
@JvmName("processAccount")
fun process(id: AccountId) = id.value
fun main() { println(process(AccountId(1))) } // 1
```
Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
## Равенство
Сравнение идёт по содержимому, причём без упаковки:
```kotlin
@JvmInline value class UserId(val value: Int)
fun main() {
println(UserId(1) == UserId(1)) // true
println(UserId(1) == UserId(2)) // false
println(setOf(UserId(1), UserId(1)).size) // 1
}
```
`equals`/`hashCode` генерируются автоматически и переопределять их нельзя.
## value class и data class
```kotlin
@JvmInline value class Meters(val value: Double)
data class Point(val x: Int, val y: Int)
```
`value class` - одно поле, без аллокации, нет `copy`. `data class` - любое число полей, всегда объект, есть `copy` и `componentN`.
Для одного поля `data class` тоже работает, но создаёт объект:
```kotlin
data class SlowId(val value: Int) // объект на каждый вызов
@JvmInline value class FastId(val value: Int) // int
fun main() {
println(SlowId(1) == SlowId(1)) // true, но два объекта были созданы
println(FastId(1) == FastId(1)) // true, объектов не было
}
```
## typealias - не замена
```kotlin
typealias UserId2 = Int
fun load(id: UserId2) = id
fun main() {
println(load(42)) // 42 — любое Int подходит, защиты нет
}
```
`typealias` - только псевдоним, он не создаёт новый тип и не защищает от путаницы. Подробнее: [Typealias](Typealias.md)
> [!info] Отличия от Java
> Прямого аналога нет. Java-разработчик выбирает между:
> - **обычным классом-обёрткой** - типобезопасно, но аллокация на каждый объект
> - **примитивом с комментарием** - быстро, но ошибки порядка аргументов не ловятся
>
> ```java
> // обёртка: безопасно, но объект
> final class UserId {
> private final int value;
> UserId(int value) { this.value = value; }
> // equals, hashCode, геттер...
> }
> ```
>
> В Java это закрывает Project Valhalla (`value class` в JEP 401), но он ещё не вышел в релиз. Пока единственный способ получить безопасность без аллокации - `value class` Kotlin.
>
> | Java | Kotlin |
> |---|---|
> | класс-обёртка + аллокация | `@JvmInline value class` |
> | `int` напрямую | `value class` с тем же байт-кодом |
> | Valhalla (в разработке) | работает с Kotlin 1.5 |
>
> Сравнить: [Классы-обертки в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Классы-обертки.md)
> [!warning] Подводные камни
> **`@JvmInline` обязательна.** Без неё на JVM компилятор выдаст ошибку, а не предупреждение:
> ```kotlin
> // value class Bad(val x: Int) // error: value class without @JvmInline
> @JvmInline value class Good(val x: Int)
>
> fun main() { println(Good(1).x) }
> ```
>
> **Nullable-тип отменяет инлайнинг.** `UserId?` - настоящий объект:
> ```kotlin
> @JvmInline value class UserId(val value: Int)
>
> fun find(id: UserId?): String = id?.value?.toString() ?: "нет"
>
> fun main() { println(find(UserId(1))) } // 1 — но обёртка была создана
> ```
> В горячем коде вместо `UserId?` лучше использовать значение-маркер.
>
> **В коллекциях выгоды нет.** `List<UserId>` хранит объекты из-за стирания типов.
>
> **`init` вызывается, но не защищает от Java.** Java-код может создать экземпляр в обход проверки, потому что на уровне байт-кода это просто `int`:
> ```kotlin
> @JvmInline
> value class Age(val value: Int) {
> init { require(value >= 0) { "возраст отрицательный" } }
> }
>
> fun main() {
> println(Age(5).value) // 5
> // Age(-1) // IllegalArgumentException из Kotlin
> }
> ```
>
> **Mangling ломает вызов из Java и из фреймворков, работающих через рефлексию.** Jackson, Spring, JPA требуют отдельной настройки для `value class`; иногда он просто не поддерживается.
>
> **Переопределить `equals`, `hashCode` нельзя** - они генерируются. `toString` переопределить можно.
>
> **Два `value class` над одним типом взаимозаменяемы в байт-коде.** Приведение через `Any` обойдёт проверку типов:
> ```kotlin
> @JvmInline value class Meters(val v: Int)
> @JvmInline value class Feet(val v: Int)
>
> fun main() {
> val m = Meters(10)
> val any: Any = m
> println((any as Meters).v) // 10
> // println((any as Feet).v) // ClassCastException — на уровне Kotlin проверка есть
> }
> ```
>
> **Нельзя наследоваться и нельзя быть `open`** - иерархии из `value class` не построить.
Дополнительно: [Typealias](Typealias.md)
Далее: [Object и companion object](Object%20и%20companion%20object.md)