Встроенные классы
Иногда для бизнес-логики необходимо создать оболочку вокруг некоторого типа. Однако это вносит накладные расходы во время выполнения из-за дополнительных выделений памяти. Более того, если оборачиваемый тип является примитивным, потеря производительности ужасна, поскольку примитивные типы обычно сильно оптимизируются во время выполнения, в то время как их оболочки не получают никакого особого обращения.
Для решения таких проблем Kotlin вводит специальный вид класса, называемый встроенным классом. Встроенные классы являются подмножеством классов на основе значений. У них нет идентичности, и они могут содержать только значения.
Для объявления встроенного класса используйте модификатор value перед именем класса:
value class Password(private val s: String)
Для объявления встроенного класса для JVM-бекенда используйте модификатор value вместе с аннотацией @JvmInline перед объявлением класса:
// For JVM backends @JvmInline value class Password(private val s: String)
Встроенный класс должен иметь единственное свойство, инициализированное в первичном конструкторе. Во время выполнения экземпляры встроенного класса будут представлены с помощью этого единственного свойства (см. подробности о представлении во время выполнения ниже):
// No actual instantiation of class 'Password' happens
// At runtime 'securePassword' contains just '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() // method `greet` is called as a static method
println(name.length) // property getter is called as a static method
}
Свойства встроенных классов не могут иметь поддерживающих полей. Они могут иметь только простые вычисляемые свойства (без 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()) // Still called as a static method
}
Запрещается участие встроенных классов в иерархии классов. Это означает, что встроенные классы не могут расширять другие классы и всегда 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) // 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)
}
Поскольку встроенные классы могут быть представлены как основное значение, так и как оболочка, ссылочная эквивалентность для них бессмысленна и поэтому запрещена.
Встроенные классы также могут иметь параметр обобщения в качестве основного типа. В этом случае компилятор сопоставляет его с Any? или, вообще, с верхней границей параметра типа.
@JvmInline
value class UserId<T>(val value: T)
fun compute(s: UserId<String>) {} // compiler generates fun compute-<hashcode>(s: Any?)
Искажение
Поскольку встроенные классы компилируются в их основной тип, это может привести к различным неясностям, например, к неожиданным столкновениям сигнатуры платформы:
@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) { }
Для смягчения таких проблем функции, использующие встроенные классы, искажаются путем добавления некоторого стабильного хэш-кода к имени функции. Следовательно, 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) { }
Встроенные классы против псевдонимов типов
На первый взгляд, встроенные классы очень похожи на псевдонимы типов. Действительно, оба кажутся вводящими новый тип, и оба будут представлены как основной тип во время выполнения.
Однако ключевое различие состоит в том, что псевдонимы типов совместимы по присваиванию со своим основным типом (и с другими псевдонимами типов с тем же основным типом), в то время как встроенные классы несовместимы.
Другими словами, встроенные классы вводят по-настоящему новый тип, в отличие от псевдонимов типов, которые вводят только альтернативное имя (псевдоним) для существующего типа:
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
}
Встроенные классы и делегирование
Реализация с помощью делегирования во встроенное значение встроенного класса разрешена для интерфейсов:
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