5.8.9.3 内联值类

原文链接: https://kotlinlang.org/docs/inline-classes.html

5.8.9.3 内联值类

有时把一个值包装到类中可以创建更具领域特定含义的类型。不过,这会因为额外的堆分配而带来运行时开销。而且,如果被包装的类型是基本类型,性能损失会更明显,因为基本类型通常会被运行时高度优化,而它们的包装类型则不会得到特殊对待。

为解决这类问题,Kotlin 引入了一种特殊的类,称为内联类。内联类是基于值的类的一个子集。它们没有同一性,只能保存值。

要声明内联类,请在类名之前使用 value 修饰符:

1
value class Password(private val s: String)

要为 JVM 后端声明内联类,请在类声明之前把 value 修饰符与 @JvmInline 注解一起使用:

1
2
3
// 用于 JVM 后端
@JvmInline
value class Password(private val s: String)

内联类必须在主构造函数中初始化一个属性。运行时,内联类的实例会用这唯一的属性来表示(关于运行时表示的细节见下文):

1
2
3
// 并不会真正创建 'Password' 类的实例
// 运行时 'securePassword' 中只包含 'String'
val securePassword = Password("Don't try this in production")

这是内联类的主要特性,也正是内联这个名字的由来:类的数据会被内联到使用它的位置(类似于内联函数的内容被内联到调用处)。

成员

内联类支持普通类的一部分功能。特别是,它们可以声明属性和函数,可以有 init 块和次构造函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@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() // `greet()` 函数以静态方法的形式被调用
    println(name2.length) // 属性 getter 以静态方法的形式被调用
}

内联类的属性不能有幕后字段。它们只能有简单的可计算属性(不能有 lateinit/委托属性)。

继承

内联类可以继承接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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()) // 仍以静态方法的形式被调用
}

内联类不允许参与类层级。这意味着内联类不能继承其他类,并且始终是 final 的。

表示形式

在生成的代码中,Kotlin 编译器会为每个内联类保留一个包装器。内联类实例在运行时既可以表示为包装器,也可以表示为底层类型。这类似于 Int 既可以表示为基本类型 int,也可以表示为包装类型 Integer。

Kotlin 编译器会优先使用底层类型而不是包装器,以生成性能最好、最优化后的代码。不过,有时保留包装器也是必要的。一般来说,内联类在用作其他类型时都会被装箱。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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) // 未装箱:作为 Foo 本身使用
    asGeneric(f) // 已装箱:作为泛型类型 T 使用
    asInterface(f) // 已装箱:作为类型 I 使用
    asNullable(f) // 已装箱:作为 Foo? 使用,它与 Foo 不同

    // 下面 'f' 先被装箱(传给 'id' 时),然后再被拆箱(从 'id' 返回时)
    // 最终 'c' 与 'f' 一样包含未装箱的表示(就是 '42')
    val c = id(f)
}

由于内联类既可以表示为底层值,也可以表示为包装器,引用相等性对它们没有意义,因此是被禁止的。

内联类也可以用泛型类型参数作为底层类型。在这种情况下,编译器会把它映射为 Any?,或者一般来说映射为该类型参数的上界。

1
2
3
4
@JvmInline
value class UserId<T>(val value: T)

fun compute(s: UserId<String>) {} // 编译器生成 fun compute-<hashcode>(s: Any?)

名称改编

由于内联类会被编译为它们的底层类型,这可能引发各种难以理解的错误,例如意外的平台签名冲突:

1
2
3
4
5
6
7
8
@JvmInline
value class UInt(val x: Int)

// 在 JVM 上表示为 'public final void compute(int x)'
fun compute(x: Int) { }

// 在 JVM 上也表示为 'public final void compute(int x)'!
fun compute(x: UInt) { }

为缓解这类问题,使用内联类的函数会通过给函数名添加稳定的哈希值来进行名称改编(mangling)。因此 fun compute(x: UInt) 会被表示为 public final void compute-<hashcode>(int x),从而解决了冲突问题。

从 Java 代码调用

你可以从 Java 代码调用接受内联类的函数。为此,需要手动禁用名称改编:在函数声明之前添加 @JvmName 注解:

1
2
3
4
5
6
7
@JvmInline
value class UInt(val x: Int)

fun compute(x: Int) { }

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

默认情况下,Kotlin 会使用未装箱的表示编译内联类,这让它们难以从 Java 访问。要了解如何把内联类编译为可从 Java 访问的装箱表示,请参阅从 Java 调用 Kotlin指南。

内联类与类型别名

乍看之下,内联类与类型别名非常相似。确实,两者看起来都引入了新类型,并且在运行时都会表示为底层类型。

不过,关键区别在于:类型别名与其底层类型(以及具有相同底层类型的其他类型别名)是赋值兼容的,而内联类不是。

换句话说,内联类引入的是一个真正全新的类型,而类型别名只是为已有类型引入一个替代名称(别名):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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:传入别名代替底层类型
    acceptString(nameInlineClass) // 不行:不能传入内联类代替底层类型

    // 反过来也一样:
    acceptNameTypeAlias(string) // OK:传入底层类型代替别名
    acceptNameInlineClass(string) // 不行:不能传入底层类型代替内联类
}

内联类与委托

在内联类的内联值上通过委托实现接口是允许的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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() {
            // 函数体
        }
    })
    println(my.foo()) // 打印 "foo"
}