Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user