
在 Kotlin 2.5.0-Beta2 之前,内联值类(value class)可以为单个值定义自定义的包装类型。如果类型需要多个属性,就只能使用普通类或数据类。
以 data class 来讲,即使两个实例包含相同的数据,仍然可以通过引用区分它们,哪怕这种区别对该类型并没有实际意义。
这样做可能限制运行时优化,也可能让代码不必要地依赖对象引用是否保持不变。
例如:
kotlin
data class Point(val x: Int, val y: Int)
val a = Point(1, 2)
val b = Point(1, 2)
val c = a
println(a == b) // true:属性值相同
println(a === b) // false:是两个不同的对象
println(a === c) // true:引用同一个对象
对于坐标,我们通常只关心它是不是 (1, 2)。至于它是第一次创建的那个对象,还是后来创建的另一个对象,对"坐标"这个概念没有意义。但普通类和 data class 仍然允许代码使用 === 来观察这种区别。
Kotlin 2.5.0-Beta2 引入了实验性的完整值类(full value classes),这些类型的值由各个属性共同构成,实例没有可供依赖的身份。直接使用 === 比较这些值会被编译器拒绝:你不能依赖它们是不是同一个对象来区分它们。
例如,可以声明一个完整值类 Complex,用实部和虚部两个属性表示复数:
kotlin
value class Complex(val real: Double, val imaginary: Double)
与 data class 类似,编译器会为完整值类自动生成 equals()、hashCode() 和 toString()。
不过,有两点与 data class 不同:
- 不生成
componentN()函数。但可以使用基于名称的解构,无需额外启用。 - 不生成
copy()函数。未来可以改用copy vars,这个我们暂时不讲。
抽象值类
完整值类可以是抽象的。
与接口不同的是,它们可以定义初始化逻辑,每个子类在构造时都会执行这些逻辑。
例如,下面的 OrderedInterval 值类通过构造参数接收 start 和 end。IntInterval 值类则将这些值存储在自己的 start 和 end 属性中。
kotlin
sealed value class OrderedInterval<T : Comparable<T>> protected constructor(
start: T,
end: T,
) {
init {
require(start <= end)
}
}
value class IntInterval(
val start: Int,
val end: Int
) : OrderedInterval<Int>(start, end)
值对象
完整值类也可以没有属性,可以使用 value object 声明值对象(value objects)。
它适合表示只有一个可能的值、且没有实例身份的类型,例如状态或令牌:
kotlin
sealed interface UserResponse
value class UserFound(val user: User) : UserResponse
value object UserUnknown : UserResponse
如何启用
该功能仍处于实验阶段。
如果想尝鲜启用它,需要在构建文件中添加以下编译器选项:
kotlin
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xfull-value-classes")
}
}
类声明的变化
引入这些新值类后,value class 不再一定意味着内联。
内联值类在运行时可能直接使用它包装的值。这种表示方式还可能影响 JVM 方法签名和序列化结果等外部行为。
完整值类则不遵循同样的内联表示规则,因此 Spring 和序列化库等工具对这两种类型的处理方式可能不同。
为了明确区分两者,按照目前的迁移计划,未来编译器将要求使用 inline 修饰符,或采用 @JvmInline value class 的声明方式,显式标记内联值类。仅使用 value class 的声明将不再被视为内联值类。
Kotlin 团队计划分阶段调整这一行为:
- Kotlin 2.5.20 将对未显式标记的单字段值类发出警告。
- Kotlin 2.6.0 或 2.7.0 将把该警告升级为错误。
可以使用 -Xinline-value-class-migration=warning 和 -Xinline-value-class-migration=strict 编译器选项,提前为这一变化做好准备。
为库中的类迁移到值类做准备
作为库作者,可以使用 @WillBecomeValue 注解,为将来把库中的普通类迁移到完整值类做准备。当使用者的代码依赖对象身份时,例如使用引用相等比较(===)、同步操作或身份哈希码,编译器会发出警告。这些警告能让使用者在类迁移之前,有时间调整自己的代码。
该注解仍处于实验阶段,需要通过 @OptIn(ExperimentalValueClassApi::class) 显式启用。库的使用者使用带有该注解的类时,不需要额外启用。