说起存储模型(model)时,Kotlin 的数据类( data class
) 是我们的第一选择。数据类加上一系列必要的方法,使得开发人员的编码效率得到了很大的提升。Kotlin 1.5 引入了 值类(value class
)。这是什么类型的类,我们又该何时使用它呢?
当数据类用于保存模型时,值类将属性添加到值中并约束其使用。该类只是一个值的包装器,但是 Kotlin 编译器可以确保没有因包装而产生任何内存开销。
问题
持续时间(Duration
)是一个经典问题,如果不阅读注释或者源代码,任何人都可能用错。 所以,我编写了一个在给定以毫秒为单位的持续时间内显示提示消息的函数,函数签名如下所示:
fun showTooltip(message: String, duration: Long) { ... }
为了确保调用方传入正确的持续时间,我可以在参数名中提示:
fun showTooltip(message: String, durationInMillis: Long) { ... }
甚至加上注释:
/**
* Shows tooltip of message for given duration
* @param message - message to display
* @param durationInMillis - duration in milliseconds
**/
fun showTooltip(message: String, durationInMillis: Long) { ... }
尽管有了清晰的命名和说明文档,仍可能有人调用函数时传入以秒为单位的持续时间。毕竟,美国宇航局就曾由于一个单位错误而损失了一艘航天器。
showTooltip("I'm going to pass duration in seconds", 2L)
一个简单的解决方案
为了确保调用方以毫秒为单位传递参数,可以将持续时间包装在一个类中,并提供帮助方法限制对象的创建。这个包装类可以确保向函数传递正确的值。
class Duration private constructor (
val millis: Long
) {
companion object {
fun millis(millis: Long) = Duration(millis)
fun seconds(seconds: Long) = Duration(seconds * 1000)
}
}
fun showTooltip(message: String, duration: Duration) {
println("Will show $message for ${duration.millis} milliseconds")
}
...
showTooltip("Hello - Seconds", Duration.seconds(2L))
showTooltip("Hello - Millis", Duration.millis(1200))
...
现在 showTooltip
函数接收持续时间并以毫秒为单位进行处理。这确保调用者向函数传入正确的持续时间,showTooltip
可以信赖持续时间是以毫秒为单位。
然而,每个持续时间要用一个对象包装起来以避免歧义。这样每传入一个参数,都会创建一个新对象并为其额外分配内存。
使用 kotlin 值类
// Invoking the function that consumes value class param.
showTooltip_fxiZ0zM("",
Duration.Companion.millis_PZfE49U(2000L));
showTooltip_fxiZ0zM("",
Duration.Companion.seonds_PZfE49U(2L));
Kotlin 值类通过包装单个值来对该值进行限制或者转换。Kotlin 编译器可以在某些情况下取消装箱以保证性能。
@JvmInline
value class Duration private constructor (
val millis: Long
) {
companion object {
fun millis(millis: Long) = Duration(millis)
fun seconds(seconds: Long) = Duration(seconds * 1000)
}
}
可以看到,值类和普通类的区别只在于多了两个新的关键字。但此时正常持续时间类和值类是完全不同的。这是值类编译后的字节码:
// Duration -- value class bytecode
public final class Duration {
private final long millis;
private Duration(long millis) {
this.millis = millis;
}
public static final class Companion {
// Comapanion that outputs Duration is mangled to return the wrapped value
public final long millis_PZfE49U/* $FF was: millis-PZfE49U*/(long millis) {
return Duration.constructor-impl(millis);
}
// Comapanion that outputs Duration is mangled to return the wrapped value
public final long seconds_PZfE49U/* $FF was: seconds-PZfE49U*/(long seconds) {
return Duration.constructor-impl(seconds * (long)1000);
}
}
}
// Caller function name mangled
public static final void showTooltip_fxiZ0zM/* $FF was: showTooltip-fxiZ0zM*/(@NotNull String message, long duration) {
...
}
如上,在使用 Duration 对象的地方,函数名被打乱了,参数类型被改为 Long
类型。这里打乱函数名是为了防止方法重载冲突。
// Using a wrapped value arg
fun showTooltip(message: String, duration: Duration) {...}
// Using a primitive value as arg
fun showTooltip(message: String, duration: Long) {...}
如以上代码所示,开发者还定义了一个同名函数,且函数参数类型是基本类型 Long
如果。如果编译器只将参数类型替换为基本类型,这段代码就无法编译通过。所以有必要把函数名打乱。
普通包装类的字节码
public final class Duration {
private final long millis;
private Duration(long millis) {
this.millis = millis;
}
public static final class Companion {
// Companion function return the wrapped object
public final Duration millis(long millis) {
return new Duration(millis, (DefaultConstructorMarker)null);
}
// Companions function return the wrapped object
public final Duration seconds(long seconds) {
return new Duration(seconds * (long)1000, (DefaultConstructorMarker)null);
}
}
}
// Called function signature looks same
public static final void showTooltip(String message, Duration duration)
{ ...}
// Caller is
showTooltip("", Duration.Companion.seconds(2L));
showTooltip("", Duration.Companion.millis(1200L));
这是普通包装类的字节码。肯定想问为什么使用普通类而不是数据类呢?请继续往下看。这是数据类定义:
data class Duration private constructor(
val millis: Long
) {
// Same companion as normal class
}
这是数据类编译后的字节码:
public final class Duration {
private Duration(long millis) {
this.millis = millis;
}
public final long component1() {
return this.millis;
}
@NotNull
public final Duration copy(long millis) {
return new Duration(millis);
}
// Same companion byte code from normal class
...
}
可以看到,数据类会生成一个 copy
方法,该方法允许在给定 long 类型参数时创建一个新的持续时间对象。而我们的目标是用伴生对象来限制对象的创建。此外,数据类还保留了一个多余的 componentN
函数。我们仅仅只想包装值而已。
尾注
Kotlin 1.2.xx 引入了内联类(inline clasas
),内联类是值类的旧称。由于相比于内联函数,内联类实际上并没有内联,因此官方将其重命名为 value class
,现在内联关键字已被废弃。
原文链接:Kotlin value class