行內值類別 (Inline value classes)
有時,將數值包裝在類別中以建立更特定於網域的類型會很有用。但是,它會因額外的堆積記憶體配置而引入執行時額外負荷。此外,如果包裝的類型是基本類型,則效能損失會很顯著,因為基本類型通常由執行時高度最佳化,而它們的包裝器 (wrapper) 不會獲得任何特殊處理。
為了要解決這些問題,Kotlin 引入了一種特殊的類別,稱為 inline class(內聯類別)。內聯類別是 value-based classes(基於值的類別)的一個子集。它們沒有識別標記 (identity),只能保存數值。
要宣告一個內聯類別,請在類別名稱之前使用 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")
這是內聯類別的主要特點,它啟發了 inline(內聯)這個名稱:類別的資料會 inlined(內聯)到它的用法中(類似於 inline functions(內聯函式)的內容如何內聯到呼叫點)。
成員 (Members)
內聯類別支援常規類別的一些功能。特別是,它們允許宣告屬性和函式,具有 init
區塊和 secondary constructors(次要建構函式):
@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
}
內聯類別屬性不能有 backing fields(後備欄位)。它們只能有簡單的可計算屬性(沒有 lateinit
/委託屬性)。
繼承 (Inheritance)
內聯類別允許從介面繼承:
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
(最終類別)。
表示 (Representation)
在產生的程式碼中,Kotlin 編譯器為每個內聯類別保留一個 wrapper(包裝器)。內聯類別實例可以在執行時表示為包裝器或作為底層類型。這類似於 Int
如何 represented(表示)為基本類型 int
或作為包裝器 Integer
。
Kotlin 編譯器會優先使用底層類型而不是包裝器,以產生效能最高且最佳化的程式碼。但是,有時有必要保留包裝器。作為經驗法則,當內聯類別用作另一種類型時,它們會被裝箱 (boxed)。
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)
}
由於內聯類別可以表示為底層數值和包裝器,因此 referential equality(引用相等性)對它們來說毫無意義,因此是被禁止的。
內聯類別也可以具有泛型類型參數作為底層類型。在這種情況下,編譯器會將其映射到 Any?
,或者通常映射到類型參數的上限。
@JvmInline
value class UserId<T>(val value: T)
fun compute(s: UserId<String>) {} // compiler generates fun compute-<hashcode>(s: Any?)
Mangling(名稱修飾)
由於內聯類別會編譯為它們的底層類型,因此可能會導致各種模糊的錯誤,例如意外的平台簽章衝突:
@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) { }
為了減輕這些問題,使用內聯類別的函式會被 mangled(修飾),方法是在函式名稱中新增一些穩定的雜湊碼 (hashcode)。因此,fun compute(x: UInt)
將表示為 public final void compute-<hashcode>(int x)
,這解決了衝突問題。
從 Java 程式碼呼叫 (Calling from Java code)
您可以從 Java 程式碼呼叫接受內聯類別的函式。為此,您應該手動停用名稱修飾:在函式宣告之前新增 @JvmName
註解:
@JvmInline
value class UInt(val x: Int)
fun compute(x: Int) { }
@JvmName("computeUInt")
fun compute(x: UInt) { }
內聯類別 vs 類型別名 (Inline classes vs type aliases)
乍看之下,內聯類別看起來與 type aliases(類型別名)非常相似。事實上,兩者似乎都引入了一種新類型,並且兩者都將在執行時表示為底層類型。
但是,關鍵的區別在於類型別名與它們的底層類型(以及具有相同底層類型的其他類型別名)是 assignment-compatible(賦值相容),而內聯類別則不然。
換句話說,內聯類別引入了一種真正的 new(新)類型,與類型別名相反,類型別名僅為現有類型引入一個替代名稱(別名):
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 classes and delegation)
允許透過委託將介面的實作委託給內聯類別中內聯的值:
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"
}