Встроенные (inline) классы

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

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

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

value class Password(private val s: String)

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

// Для JVM backends
@JvmInline
value class Password(private val s: String)

Модификатор inline для встроенных классов не рекомендуется.

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

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

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

Члены

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

@JvmInline
value class Name(val s: String) {
    init {
        require(s.length > 0) { }
    }

    val length: Int
        get() = s.length

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

fun main() {
    val name = Name("Kotlin")
    name.greet() // метод `greet` вызывается как статический метод
    println(name.length) // геттер вызывается как статический метод
}

Свойства встроенного класса не могут иметь теневые поля. Они могут иметь только простые вычислимые свойства (без 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 сохраняет обертку для каждого встроенного класса. Экземпляры встроенного класса могут быть представлены во время выполнения либо как обертки, либо как базовый тип. Это похоже на то, как Int может быть представлен либо в виде примитива int, либо в виде обертки Integer.

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

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)    // не обернутый: используется сам Foo
    asGeneric(f)   // обернутый: используется в качестве универсального типа T
    asInterface(f) // обернутый: используется в качестве типа I
    asNullable(f)  // обернутый: используется в качестве Foo?, который отличается от Foo

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

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

Искажение

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

@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) { }

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

Схема искажения была изменена в Котлине 1.4.30. Используйте флаг компилятора -Xuse-14-inline-classes-mangling-scheme, чтобы заставить компилятор использовать старую схему искажения 1.4.0 и сохранить двоичную совместимость.

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

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

@JvmInline
value class UInt(val x: Int)

fun compute(x: Int) { }

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

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

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

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

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

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) // Всё хорошо: передача псевдонима вместо базового типа
    acceptString(nameInlineClass) // Всё плохо: не удается передать встроенный класс вместо базового типа

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