Встроенные классы
Иногда для бизнес-логики необходимо создать обёртку вокруг некоторого типа. Однако это вносит накладные расходы во время выполнения из-за дополнительных выделений памяти. Более того, если обернутый тип является примитивным, потери производительности ужасны, потому что примитивные типы обычно сильно оптимизированы во время выполнения, в то время как их обёртки не получают никакого специального обращения.
Для решения таких проблем 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)
}
Поскольку встроенные классы могут быть представлены как основное значение, так и как обёртка, ссылочная эквивалентность для них бессмысленна и поэтому запрещена.
Изменение имён
Поскольку встроенные классы компилируются в их основной тип, это может привести к различным неясным ошибкам, например, к неожиданным конфликтам сигнатур платформы:
@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
}
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/inline-classes.html