266 lines
11 KiB
Markdown
266 lines
11 KiB
Markdown
---
|
||
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)
|