Классы значений 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"
}
© 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