值类 (Value Classes)
在领域驱动设计(DDD)中,我们经常为了增加类型安全而包装基本类型(例如,用 Password 类包装 String,用 UserId 类包装 Int )。这种做法虽然安全,但会带来运行时开销:每个包装实例都需要在堆上分配内存。
Kotlin 的 值类 (Value Classes)(原名内联类 inline class)完美解决了这一痛点:它在编译时提供类型安全,在运行时实现零开销。
定义与语法
值类通过 value 关键字定义,并且在 JVM 上必须添加 @JvmInline 注解。
kotlin
@JvmInline
value class Password(val s: String)核心约束
为了实现内联,值类必须遵守严格规则:
- 必须有且仅有一个只读主构造参数(
val)。 - 不能有
init块。 - 不能有幕后字段(backing field)。
- 不能继承其他类(但可以实现接口)。
运行时表现:零开销
值类的核心黑魔法在于:在大多数情况下,它会被编译为其底层的原始类型。
kotlin
fun login(pwd: Password) {
println(pwd.s)
}
val p = Password("123456")
login(p)java
// Password 类型消失了,直接传递 String
public static void login(String pwd) {
println(pwd);
}
String p = "123456";
login(p);值类 vs 类型别名 (Typealias)
这是很多开发者容易混淆的点。typealias 仅仅是给类型起了个新名字,它不提供类型安全保护。
kotlin
typealias Username = String
typealias PasswordAlias = String
fun auth(u: Username, p: PasswordAlias) {
...
}
val u: Username = "Viro"
val p: PasswordAlias = "123"
// ❌ 危险!编译器不会报错,因为它们本质上都是 String
auth(p, u)相比之下,值类创造了一个全新的类型:
kotlin
@JvmInline
value class UserID(val id: String)
@JvmInline
value class OrderID(val id: String)
fun query(uid: UserID) {
...
}
// ✅ 安全!编译器报错:Type mismatch
// query(OrderID("123"))装箱与拆箱 (Boxing & Unboxing)
虽然值类通常是内联的,但在某些情况下,它必须被装箱(创建一个包装对象),就像 int 变成 Integer 一样。
何时发生装箱?
- 当值类作为泛型参数传递时(如
List<Password>)。 - 当值类作为可空类型使用时(如
Password?)。 - 当值类被当作其实现的接口类型使用时。
kotlin
val p = Password("123") // 拆箱 (String)
// 装箱!因为集合只存储对象引用
val list = listOf(p)
// 装箱!因为需要多态调用
val printable: IPrintable = p名字修饰 (Mangling) 互操作性
由于值类在编译时会被替换为底层类型,这可能导致函数签名冲突。
kotlin
@JvmInline
value class UserId(val id: String)
@JvmInline
value class Name(val s: String)
// 编译后变成:fun handle(String)
fun handle(u: UserId) {}
// 编译后也变成:fun handle(String) —— 冲突!
fun handle(n: Name) {}为了解决这个问题,Kotlin 编译器会对使用值类作为参数的函数名进行修饰 (Mangling),即在函数名后添加一串哈希值(如 handle-53412)。
Java 调用限制
由于名字被修饰,Java 代码默认无法直接调用这些 Kotlin 函数。如果必须供 Java 调用,需手动处理包装逻辑或避免使用值类。
实战:标准库中的无符号整数
Kotlin 标准库中的无符号类型(UInt, ULong, UByte, UShort)正是通过值类实现的。
kotlin
@JvmInline
public value class UInt(public val data: Int) : Comparable<UInt> { ... }这保证了 UInt 在运行时通常就是原生的 Java int,拥有极高的性能。
总结
- 类型安全:比
typealias更强,防止“参数传反”的低级错误。 - 性能卓越:在热点代码路径中,消除了对象分配的压力。
- 适用场景:
- ID 包装 (
UserId,ProductId) - 物理单位 (
Meters,Seconds) - 敏感数据 (
Password,Token)
- ID 包装 (
- 互操作性:注意 Java 调用的限制。