Встроенные классы значений (inline value classes)

Иногда полезно обернуть значение в класс, чтобы создать более предметно-ориентированный тип. Однако это приводит к накладным расходам во время выполнения из-за дополнительных выделений в куче. Более того, если обернутый тип является примитивным, влияние на производительность значительно: примитивные типы обычно сильно оптимизируются средой выполнения, а их обертки не получают специальной обработки.

Чтобы решить такие проблемы, Kotlin вводит специальный вид класса, который называется inline class (встроенный класс). Встроенные классы являются подмножеством классов, основанных на значениях. У них нет идентичности, и они могут только хранить значения.

Чтобы объявить встроенный класс, используйте модификатор value перед именем класса:

value class Password(private val s: String)

Чтобы объявить встроенный класс для JVM-бэкенда, используйте модификатор value вместе с аннотацией @JvmInline перед объявлением класса:

// Для JVM-бэкендов
@JvmInline
value class Password(private val s: String)

Встроенный класс должен иметь одно свойство, инициализированное в основном конструкторе. Во время выполнения экземпляры встроенного класса будут представлены с помощью этого единственного свойства (подробнее о представлении во время выполнения ниже):

// Фактически, создание экземпляра класса 'Password' не происходит
// Во время выполнения 'securePassword' содержит только 'String'
val securePassword = Password("Don't try this in production")

Это основная особенность встроенных классов, которая и дала название inline: данные класса встраиваются в места его использования, подобно тому, как содержимое встроенных функций встраивается в места вызова.

Члены

Встроенные классы поддерживают некоторые возможности обычных классов. В частности, им разрешено объявлять свойства и функции, иметь блок init и вторичные конструкторы:

@JvmInline
value class Person(private val fullName: String) {
    init {
        require(fullName.isNotEmpty()) {
            "Full name shouldn't be empty"
        }
    }

    constructor(firstName: String, lastName: String) : this("$firstName $lastName") {
        require(lastName.isNotBlank()) {
            "Last name shouldn't be empty"
        }
    }

    val length: Int
        get() = fullName.length

    fun greet() {
        println("Hello, $fullName")
    }
}

fun main() {
    val name1 = Person("Kotlin", "Mascot")
    val name2 = Person("Kodee")
    name1.greet() // функция `greet()` вызывается как статический метод
    println(name2.length) // геттер свойства вызывается как статический метод
}

{kotlin-runnable=“true” kotlin-min-compiler-version=“1.9”}

Свойства встроенного класса не могут иметь теневые поля. Они могут быть только простыми вычисляемыми свойствами (без lateinit/делегированных свойств).

Наследование

Встроенным классам разрешено наследоваться от интерфейсов:

interface Printable {
    fun prettyPrint(): String
}

@JvmInline
value class Name(val s: String) : Printable {
    override fun prettyPrint(): String = "Let's $s!"
}

fun main() {
    val name = Name("Kotlin")
    println(name.prettyPrint()) // Все еще вызывается как статический метод
}

Встроенным классам запрещено участвовать в иерархии классов. Это означает, что встроенные классы не могут расширять другие классы и всегда являются final.

Представление

В сгенерированном коде компилятор Kotlin сохраняет обертку (wrapper) для каждого встроенного класса. Экземпляры встроенного класса во время выполнения могут быть представлены либо как обертки, либо как базовый тип. Это похоже на то, как Int может быть представлен либо примитивом int, либо оберткой Integer.

Компилятор Kotlin предпочитает использовать базовые типы вместо оберток, чтобы создавать максимально производительный и оптимизированный код. Однако иногда обертки нужно сохранять. Общее правило: встроенные классы упаковываются (boxed), когда используются как другой тип.

interface I

@JvmInline
value class Foo(val i: Int) : I

fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}

fun <T> id(x: T): T = x

fun main() {
    val f = Foo(42)

    asInline(f)    // без упаковки (unboxed): используется как сам Foo
    asGeneric(f)   // с упаковкой (boxed): используется как обобщенный тип T
    asInterface(f) // с упаковкой (boxed): используется как тип I
    asNullable(f)  // с упаковкой (boxed): используется как Foo?, который отличается от Foo

    // Ниже 'f' сначала упаковывается при передаче в 'id',
    // а затем распаковывается при возврате из 'id'.
    // В итоге 'c' содержит представление без упаковки (просто '42'), как и 'f'
    val c = id(f)
}

Поскольку встроенные классы могут быть представлены и как базовое значение, и как обертка, равенство по ссылке для них бессмысленно и поэтому запрещено.

У встроенных классов также может быть обобщенный параметр типа в качестве базового типа. В этом случае компилятор сопоставляет его с Any? или, в общем случае, с верхней границей параметра типа.

@JvmInline
value class UserId<T>(val value: T)

fun compute(s: UserId<String>) {} // компилятор генерирует fun compute-<hashcode>(s: Any?)

Манглирование имен

Поскольку встроенные классы компилируются в их базовый тип, это может приводить к различным неочевидным ошибкам, например к неожиданным конфликтам платформенных сигнатур:

@JvmInline
value class UInt(val x: Int)

// Представлен как 'public final void compute(int x)' на JVM
fun compute(x: Int) { }

// Тоже представлен как 'public final void compute(int x)' на JVM!
fun compute(x: UInt) { }

Чтобы смягчить такие проблемы, функции, использующие встроенные классы, подвергаются манглированию (mangling): к имени функции добавляется стабильный хэш-код. Поэтому fun compute(x: UInt) будет представлена как public final void compute-<hashcode>(int x), что решает проблему конфликта.

Вызов из Java-кода

Из Java-кода можно вызывать функции, которые принимают встроенные классы. Для этого нужно вручную отключить манглирование: добавьте аннотацию @JvmName перед объявлением функции:

@JvmInline
value class UInt(val x: Int)

fun compute(x: Int) { }

@JvmName("computeUInt")
fun compute(x: UInt) { }

По умолчанию Kotlin компилирует встроенные классы с использованием представлений без упаковки (unboxed representations), из-за чего к ним сложно обращаться из Java. Чтобы узнать, как скомпилировать встроенные классы в доступные из Java упакованные представления (boxed representations), смотрите руководство Вызов Kotlin из Java.

Встроенные классы и псевдонимы типов

На первый взгляд встроенные классы очень похожи на псевдонимы типов. Действительно, и те и другие как будто вводят новый тип, и оба во время выполнения будут представлены базовым типом.

Однако ключевое различие заключается в том, что псевдонимы типов совместимы по присваиванию со своим базовым типом (и с другими псевдонимами типов с тем же базовым типом), а встроенные классы - нет.

Другими словами, встроенные классы вводят действительно новый тип, в отличие от псевдонимов типов, которые вводят только альтернативное имя (псевдоним) для существующего типа:

typealias NameTypeAlias = String

@JvmInline
value class NameInlineClass(val s: String)

fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}

fun main() {
    val nameAlias: NameTypeAlias = ""
    val nameInlineClass: NameInlineClass = NameInlineClass("")
    val string: String = ""

    acceptString(nameAlias) // OK: передается псевдоним вместо базового типа
    acceptString(nameInlineClass) // Ошибка: нельзя передать встроенный класс вместо базового типа

    // И наоборот:
    acceptNameTypeAlias(string) // OK: передается базовый тип вместо псевдонима
    acceptNameInlineClass(string) // Ошибка: нельзя передать базовый тип вместо встроенного класса
}

Встроенные классы и делегирование

Для интерфейсов разрешена реализация через делегирование встроенному значению встроенного класса:

interface MyInterface {
    fun bar()
    fun foo() = "foo"
}

@JvmInline
value class MyInterfaceWrapper(val myInterface: MyInterface) : MyInterface by myInterface

fun main() {
    val my = MyInterfaceWrapper(object : MyInterface {
        override fun bar() {
            // тело
        }
    })
    println(my.foo()) // выводит "foo"
}