Spec-Zone.ru › Kotlin 1.9

Классы значений inline

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

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

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

value class Password(private val s: String)

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

// For JVM backends
@JvmInline
value class Password(private val s: String)

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

// No actual instantiation of class 'Password' happens
// At runtime 'securePassword' contains just 'String'
val securePassword = Password("Don't try this in production") 

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

Члены

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() // the `greet()` function is called as a static method
    println(name2.length) // property getter is called as a static method
}

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

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

Inline-классы могут наследоваться от интерфейсов:

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()) // Still called as a static method
}

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

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

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

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

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: used as Foo itself
    asGeneric(f)   // boxed: used as generic type T
    asInterface(f) // boxed: used as type I
    asNullable(f)  // boxed: used as Foo?, which is different from Foo
    
    // below, 'f' first is boxed (while being passed to 'id') and then unboxed (when returned from 'id') 
    // In the end, 'c' contains unboxed representation (just '42'), as 'f' 
    val c = id(f)  
}

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

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

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

fun compute(s: UserId<String>) {} // compiler generates fun compute-<hashcode>(s: Any?)

Искажение

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

@JvmInline
value class UInt(val x: Int)

// Represented as 'public final void compute(int x)' on the JVM
fun compute(x: Int) { }

// Also represented as 'public final void compute(int x)' on the JVM!
fun compute(x: UInt) { }

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

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

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

@JvmInline
value class UInt(val x: Int)

fun compute(x: Int) { }

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

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

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

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

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

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: pass alias instead of underlying type
    acceptString(nameInlineClass) // Not OK: can't pass inline class instead of underlying type

    // And vice versa:
    acceptNameTypeAlias(string) // OK: pass underlying type instead of alias
    acceptNameInlineClass(string) // Not OK: can't pass underlying type instead of inline class
}

Inline-классы и делегирование

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

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() {
            // body
        }
    })
    println(my.foo()) // prints "foo"
}
Последнее изменение: 06 июля 2023 г.
Классы перечислений Объектные выражения и объявления

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/inline-classes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API