Skip to content

值类 (Value Classes)

源:Inline classes

在领域驱动设计(DDD)中,我们经常为了增加类型安全而包装基本类型(例如,用 Password 类包装 String,用 UserId 类包装 Int )。这种做法虽然安全,但会带来运行时开销:每个包装实例都需要在堆上分配内存。

Kotlin 的 值类 (Value Classes)(原名内联类 inline class)完美解决了这一痛点:它在编译时提供类型安全,在运行时实现零开销。

定义与语法

值类通过 value 关键字定义,并且在 JVM 上必须添加 @JvmInline 注解。

kotlin
@JvmInline
value class Password(val s: String)

核心约束

为了实现内联,值类必须遵守严格规则:

  1. 必须有且仅有一个只读主构造参数(val)。
  2. 不能init 块。
  3. 不能有幕后字段(backing field)。
  4. 不能继承其他类(但可以实现接口)。

运行时表现:零开销

值类的核心黑魔法在于:在大多数情况下,它会被编译为其底层的原始类型。

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 一样。

何时发生装箱?

  1. 当值类作为泛型参数传递时(如 List<Password>)。
  2. 当值类作为可空类型使用时(如 Password?)。
  3. 当值类被当作其实现的接口类型使用时。
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)
  • 互操作性:注意 Java 调用的限制。