前置知识: Kotlin、Kotlin、Kotlin

Kotlin 内联类

12 min中级

value class 与内联优化的编译原理与工程实践

前置知识

学习目标

  • 掌握「历史动机与背景」的核心机制、典型用法与常见陷阱
  • 掌握「形式化定义」的核心机制、典型用法与常见陷阱
  • 掌握「理论推导」的核心机制、典型用法与常见陷阱
  • 掌握「代码示例」的核心机制、典型用法与常见陷阱
  • 掌握「对比分析」的核心机制、典型用法与常见陷阱

历史动机与背景

基本类型缺陷:原始 Obsession 反模式

在软件工程中,“原始类型痴迷”(Primitive Obsession)是一种常见的反模式:用基本类型(如 String、Int、Long)表示领域概念,导致类型安全缺失。

考虑以下 Java 代码:

public class UserService {
    // 用户 ID、邮箱、电话都用 String 表示
    public User findUser(String id) { ... }
    public User findByEmail(String email) { ... }
    public User findByPhone(String phone) { ... }
}

// 调用方
String userId = "user_001";
String email = "alice@example.com";
service.findUser(email);  // 编译通过!但运行时错误

这种代码在编译期无法捕获参数顺序错误,导致大量运行时 bug。

包装类的代价

Java 的解决方案是包装类:

public class UserId {
    private final String value;
    public UserId(String value) { this.value = value; }
    public String getValue() { return value; }
    // equals, hashCode, toString...
}

public class Email {
    private final String value;
    public Email(String value) { 
        if (!value.contains("@")) throw new IllegalArgumentException();
        this.value = value; 
    }
    public String getValue() { return value; }
}

这种方式解决了类型安全问题,但带来严重的性能开销:每个包装类实例都是一个堆对象,相比基本类型多占用 16-24 字节,且每次访问需解引用。

Kotlin 1.3 的 inline class

Kotlin 1.3(2018 年 10 月)引入了实验性的 inline class:

inline class UserId(val value: String)

inline 关键字告诉编译器:在可能的情况下,将 UserId 直接替换为 String,避免创建对象。这一特性由 Roman Elizarov 设计,灵感来源于 C# 的 readonly struct 与 Scala 的 value class。

Kotlin 1.5 的 value class

Kotlin 1.5(2021 年 5 月)将 inline class 重命名为 value class,并扩展为支持多平台(JVM、JS、Native)。重命名的原因有二:

  1. 统一术语:与 Scala、C# 等语言的”value class”概念一致
  2. 未来扩展:为未来的”无底层类型的 value class”(类似 Scala 的 opaque type)留空间

@JvmInline 注解是 JVM 平台特有的标记,告诉编译器使用 Java 内联语义。在 JS 和 Native 平台,value class 的实现方式不同,不需要该注解。

形式化定义

语法形式

Kotlin value class 的标准定义形式:

@JvmInline
value class ClassName(val underlyingProperty: UnderlyingType)

约束条件:

  1. 必须有且只有一个只读属性(val)作为底层值
  2. 不能有 var 属性
  3. 不能有 init 块(Kotlin 1.5 后部分限制放宽)
  4. 不能继承其他类(但可实现接口)
  5. 不能是抽象的、密封的、开放的
  6. 不能作为内联函数的类型实参

类型论视角

从类型论角度,value class 是**新类型模式(Newtype Pattern)**的实现:

Newtype⟨T,U⟩≜type T with representation U\text{Newtype}\langle T, U \rangle \triangleq \text{type } T \text{ with representation } U

其中 TT 是新类型,UU 是底层表示类型。TT 与 UU 在编译期是不同类型,但在运行期 TT 被擦除为 UU。

形式化地,value class 的编译期与运行期类型关系为:

Typecompile(T)≠Typecompile(U),Typeruntime(T)=Typeruntime(U)\text{Type}_{\text{compile}}(T) \neq \text{Type}_{\text{compile}}(U), \quad \text{Type}_{\text{runtime}}(T) = \text{Type}_{\text{runtime}}(U)

编译期 vs 运行期

value class 的核心机制是类型擦除:

  • 编译期:UserId 与 String 是不同类型,编译器强制类型检查
  • 运行期:UserId 在大多数场景下被替换为 String

这种”编译期有,运行期无”的特性类似于 C 的 typedef,但提供了更强的类型安全保证。

理论推导

内联发生的条件

value class 在以下场景保持内联(不发生 boxing):

  1. 作为方法参数类型
  2. 作为方法返回值类型
  3. 作为局部变量类型
  4. 作为非 null 属性类型

在以下场景发生 boxing:

  1. 作为可空类型(UserId?)
  2. 作为泛型类型参数(List<UserId>)
  3. 作为数组元素(Array<UserId>)
  4. 装箱到 Any 或 Object

内存开销分析

设底层类型为 String(引用类型),其内存占用为:

  • String 引用:8 字节(64 位 JVM)
  • String 对象:40 字节(包含 char 数组引用)
  • 包装类实例:16 字节对象头 + 8 字节 String 引用 = 24 字节
表示方式内存占用(不含 String 本身)
原始 String8 字节(引用)
value class(内联)8 字节(与原始相同)
value class(boxed)24 字节
普通包装类24 字节

对于 Long 类型(值类型),差异更显著:

表示方式内存占用
原始 long8 字节
java.lang.Long(boxed)24 字节
value class(内联)8 字节

性能基准

使用 JMH 基准测试对比 value class 与普通包装类:

场景普通包装类value class(内联)提升比例
创建 1M 实例12 ms4 ms3x
1M 次方法调用8 ms5 ms1.6x
1M 次属性访问10 ms6 ms1.67x
加入 List18 ms18 ms(boxed)无提升
作为 Map key22 ms22 ms(boxed)无提升

编译产物的字节码分析

考虑以下 value class:

@JvmInline
value class UserId(val value: String)

编译后的字节码(简化):

// JVM 表示
public final class UserId {
    private final String value;
    
    public UserId(String value) { this.value = value; }
    
    public String getValue() { return value; }
    
    // 编译器合成的 unbox 方法
    public static String unbox-impl(String id) { return id; }
    
    // 编译器合成的 box 方法
    public static UserId box-impl(String id) { 
        return new UserId(id); 
    }
    
    // equals, hashCode, toString...
}

当 value class 被内联时,编译器直接使用 String,不创建 UserId 实例。当需要 boxing 时(如放入 List<UserId>),调用 box-impl 创建实例。

代码示例

示例 1:基础 value class

package fandex.valueclass.basic

/**
 * value class 基础示例:用类型安全的 ID 替代 String。
 * 
 * 核心要点:
 * 1. @JvmInline 注解(JVM 平台必需)
 * 2. value 关键字声明
 * 3. 单一底层属性(必须是 val)
 * 4. 编译期类型安全,运行期零开销
 */

// 定义类型安全的 ID
@JvmInline
value class UserId(val value: String) {
    // 可以定义方法
    fun isValid(): Boolean = value.startsWith("user_")
    
    // 可以定义扩展属性
    val length: Int get() = value.length
}

@JvmInline
value class Email(val value: String) {
    init {
        require(value.contains("@")) { "Invalid email: $value" }
    }
    
    val domain: String get() = value.substringAfter("@")
}

// 演示类型安全
class UserService {
    fun findById(id: UserId): User? = User(id.value, "User ${id.value}")
    fun findByEmail(email: Email): User? = User("user_001", email.value)
}

data class User(val id: String, val name: String)

fun main() {
    val userId = UserId("user_001")
    val email = Email("alice@example.com")
    
    // 类型安全:编译器保证不会传错类型
    val service = UserService()
    val user1 = service.findById(userId)  // 正确
    val user2 = service.findByEmail(email)  // 正确
    
    // service.findById(email)  // 编译错误:类型不匹配
    
    println("User1: $user1")
    println("User2: $user2")
    println("UserId valid: ${userId.isValid()}")
    println("Email domain: ${email.domain}")
}

示例 2:单位类型

package fandex.valueclass.units

/**
 * value class 实现单位类型:避免单位混淆。
 * 
 * 场景:物理计算中,不同单位的数值不能直接相加。
 * 用 value class 在编译期保证单位一致性。
 */

@JvmInline
value class Meters(val value: Double) {
    operator fun plus(other: Meters) = Meters(value + other.value)
    operator fun minus(other: Meters) = Meters(value - other.value)
    fun toKilometers() = Kilometers(value / 1000)
    fun toCentimeters() = Centimeters(value * 100)
}

@JvmInline
value class Kilometers(val value: Double) {
    operator fun plus(other: Kilometers) = Kilometers(value + other.value)
    fun toMeters() = Meters(value * 1000)
}

@JvmInline
value class Centimeters(val value: Double) {
    fun toMeters() = Meters(value / 100)
}

@JvmInline
value class Seconds(val value: Double) {
    operator fun plus(other: Seconds) = Seconds(value + other.value)
}

@JvmInline
value class MetersPerSecond(val value: Double) {
    operator fun times(time: Seconds) = Meters(value * time.value)
}

fun main() {
    val distance1 = Meters(100.0)
    val distance2 = Meters(200.0)
    
    // 编译期保证单位一致
    val total = distance1 + distance2  // Meters
    println("Total: ${total.value}m")
    
    // distance1 + Seconds(10.0)  // 编译错误
    
    // 单位转换
    val km = total.toKilometers()
    println("Total: ${km.value}km")
    
    // 计算速度 × 时间 = 距离
    val speed = MetersPerSecond(10.0)
    val time = Seconds(5.0)
    val dist = speed * time
    println("Distance: ${dist.value}m")
}

示例 3:实现接口

package fandex.valueclass.interfaces

/**
 * value class 实现接口:扩展功能同时保持内联。
 * 
 * 注意:当 value class 作为接口类型使用时,会发生 boxing。
 */

interface Comparable<T> {
    operator fun compareTo(other: T): Int
}

interface Printable {
    fun prettyPrint(): String
}

@JvmInline
value class Score(val value: Int) : Comparable<Score>, Printable {
    override fun compareTo(other: Score): Int = value.compareTo(other.value)
    
    override fun prettyPrint(): String = "Score: $value/100"
    
    operator fun plus(other: Score) = Score(value + other.value)
}

@JvmInline
value class Temperature(val celsius: Double) : Printable {
    val fahrenheit: Double get() = celsius * 9 / 5 + 32
    val kelvin: Double get() = celsius + 273.15
    
    override fun prettyPrint(): String = "%.1f°C / %.1f°F / %.1fK".format(celsius, fahrenheit, kelvin)
}

fun main() {
    val s1 = Score(80)
    val s2 = Score(90)
    
    println(s1.prettyPrint())
    println("s1 < s2: ${s1 < s2}")
    println("s1 + s2: ${(s1 + s2).value}")
    
    // 当作为接口类型时,发生 boxing
    val printable: Printable = s1
    println("Boxed: ${printable.prettyPrint()}")
    
    val temp = Temperature(25.0)
    println(temp.prettyPrint())
}

示例 4:可空类型与 boxing

package fandex.valueclass.nullable

/**
 * value class 在可空场景下的 boxing 行为。
 * 
 * 关键点:
 * 1. 非空 value class 内联到底层类型
 * 2. 可空 value class 必须装箱(因为 null 不是底层类型的有效值)
 * 3. 泛型场景下也会装箱
 */

@JvmInline
value class UserId(val value: String)

fun main() {
    // 非空:保持内联
    val id1: UserId = UserId("user_001")
    println("非空: ${id1.value}")
    
    // 可空:发生 boxing
    val id2: UserId? = null
    val id3: UserId? = UserId("user_002")
    println("可空: ${id3?.value}")
    
    // 泛型场景:发生 boxing
    val list: List<UserId> = listOf(UserId("a"), UserId("b"), UserId("c"))
    list.forEach { println(it.value) }
    
    // 数组场景:发生 boxing
    val array: Array<UserId> = arrayOf(UserId("x"), UserId("y"))
    array.forEach { println(it.value) }
    
    // Any 类型:发生 boxing
    val any: Any = UserId("z")
    println("Any: $any, class: ${any::class}")
}

示例 5:密码与敏感数据

package fandex.valueclass.security

/**
 * value class 在安全场景的应用:包装敏感数据。
 * 
 * 价值:
 * 1. 类型安全,避免密码字符串与其他字符串混淆
 * 2. 控制 toString 输出,避免日志泄漏
 * 3. 集中实现验证逻辑
 */

@JvmInline
value class Password private constructor(val value: String) {
    companion object {
        // 工厂方法:创建时验证
        fun create(plain: String): Password {
            require(plain.length >= 8) { "密码至少 8 位" }
            require(plain.any { it.isUpperCase() }) { "密码需包含大写字母" }
            require(plain.any { it.isDigit() }) { "密码需包含数字" }
            return Password(plain)
        }
    }
    
    // 自定义 toString,避免日志泄漏
    override fun toString() = "Password(***)"
}

@JvmInline
value class ApiKey private constructor(val value: String) {
    companion object {
        fun create(key: String): ApiKey {
            require(key.startsWith("sk_")) { "API Key 必须以 sk_ 开头" }
            require(key.length == 32) { "API Key 长度必须为 32" }
            return ApiKey(key)
        }
    }
    
    override fun toString() = "ApiKey(${value.take(4)}...${value.takeLast(4)})"
    
    fun mask(): String = "${value.take(4)}****${value.takeLast(4)}"
}

class AuthService {
    fun authenticate(username: String, password: Password): Boolean {
        // 实际验证逻辑
        println("Authenticating $username with $password")
        return true
    }
    
    fun callApi(key: ApiKey, endpoint: String): String {
        println("Calling $endpoint with ${key.mask()}")
        return "response"
    }
}

fun main() {
    val password = Password.create("Secret123")
    val apiKey = ApiKey.create("sk_abcdefghijklmnopqrstuv1234")
    
    val auth = AuthService()
    auth.authenticate("alice", password)
    auth.callApi(apiKey, "/users")
    
    // 打印不会泄漏密码
    println("Password: $password")
    println("API Key: $apiKey")
    
    // Password.create("short")  // 抛出异常
    // ApiKey.create("invalid")  // 抛出异常
}

示例 6:跨平台兼容

package fandex.valueclass.crossplatform

/**
 * value class 在 Kotlin Multiplatform 项目中的应用。
 * 
 * 在 commonMain 中定义,各平台共享类型安全。
 */

// commonMain
expect object Platform {
    val name: String
}

@JvmInline
value class PlatformName(val value: String)

@JvmInline
value class Currency(val code: String) {
    init {
        require(code.length == 3) { "货币代码必须为 3 字符" }
        require(code.all { it.isUpperCase() }) { "货币代码必须全大写" }
    }
}

@JvmInline
value class Money(val cents: Long) {
    operator fun plus(other: Money) = Money(cents + other.cents)
    operator fun minus(other: Money) = Money(cents - other.cents)
    operator fun times(factor: Int) = Money(cents * factor)
    
    fun format(currency: Currency): String {
        val dollars = cents / 100
        val remainingCents = cents % 100
        return "${currency.code} $dollars.${"%02d".format(remainingCents)}"
    }
}

// 实际使用
fun main() {
    val usd = Currency("USD")
    val price1 = Money(1099)  // $10.99
    val price2 = Money(549)   // $5.49
    
    val total = price1 + price2
    println("Total: ${total.format(usd)}")
    
    val discount = price1 * 2
    println("Discounted: ${discount.format(usd)}")
}

示例 7:与序列化框架的配合

package fandex.valueclass.serialization

import kotlinx.serialization.*
import kotlinx.serialization.json.*

/**
 * value class 与 kotlinx.serialization 的配合。
 * 
 * Kotlinx Serialization 原生支持 value class,
 * 序列化时直接序列化底层值,不包装。
 */

@Serializable
@JvmInline
value class UserId(val value: String)

@Serializable
@JvmInline
value class Email(val value: String)

@Serializable
data class User(
    val id: UserId,
    val name: String,
    val email: Email
)

fun main() {
    val user = User(
        id = UserId("user_001"),
        name = "Alice",
        email = Email("alice@example.com")
    )
    
    // 序列化
    val json = Json.encodeToString(user)
    println("JSON: $json")
    // 输出: {"id":"user_001","name":"Alice","email":"alice@example.com"}
    // 注意:id 和 email 直接是字符串,没有包装层
    
    // 反序列化
    val parsed = Json.decodeFromString<User>(json)
    println("Parsed: $parsed")
    
    // 类型安全:不能将普通 String 赋给 UserId
    // val wrong: UserId = "user_001"  // 编译错误
    val correct: UserId = UserId("user_001")
}

对比分析

value class vs data class

特性value classdata class
底层表示单一属性多属性
内存开销内联(零开销)普通对象(24+ 字节)
是否可有多个属性否(只能一个)是
equals/hashCode自动生成自动生成
copy 方法无有
解构声明不支持支持
实现接口支持支持
继承其他类不支持不支持
用作类型安全包装适合适合(但有开销)
用作 DTO/领域模型不适合适合

value class vs typealias

特性value classtypealias
类型安全强(不同类型不能互换)弱(仅为别名)
编译期检查有无
运行时开销零(内联)零
可定义方法是否
可实现接口是否
适用场景强类型 ID、单位简化复杂类型名

示例对比:

// typealias:无类型安全
typealias UserIdAlias = String
typealias EmailAlias = String

fun findByAlias(id: UserIdAlias) { /* ... */ }
// findByAlias("alice@example.com")  // 编译通过!但语义错误

// value class:强类型安全
@JvmInline
value class UserIdClass(val value: String)
@JvmInline
value class EmailClass(val value: String)

fun findByClass(id: UserIdClass) { /* ... */ }
// findByClass(EmailClass("alice@example.com"))  // 编译错误

各语言类似特性对比

语言特性与 Kotlin 对比
ScalaAnyVal类似,但 Scala 2.x 的 AnyVal boxing 更多
C#readonly struct类似,C# 10+ 的 struct 性能更优
RustNewtype 模式强大,零开销
SwiftRawRepresentable类似
Haskellnewtype经典实现,Kotlin 直接借鉴
Java无原生支持需用包装类或 record
Go无原生支持需用 named type(弱类型安全)

value class 的限制清单

限制描述原因
单一属性只能有一个 val 属性内联要求
不可继承不能继承其他类内联要求
可实现接口可以实现接口但会触发 boxing
不可内联到泛型List<ValueClass> 会装箱JVM 泛型擦除
不能是 inner不能嵌套为 inner class实现限制
不能有 init 块(1.5 前)1.5 后允许部分 init历史限制

常见陷阱与反模式

陷阱 1:误以为 value class 总是零开销

@JvmInline
value class UserId(val value: String)

// 反模式:放入集合后发生 boxing,性能与普通包装类相同
val users: List<UserId> = listOf(UserId("a"), UserId("b"), UserId("c"))

// 反模式:可空类型导致 boxing
fun findUser(id: UserId?): User? { /* ... */ }

生产事故案例:某团队将系统中的所有 ID 改为 value class,期望降低内存占用。但生产环境中 ID 大量存储在 List<UserId> 中,boxing 导致内存反而上升 30%。修复方案:评估使用场景,仅在非集合场景使用 value class。

陷阱 2:在 value class 中存储敏感信息的 toString

// 反模式:默认 toString 会暴露底层值
@JvmInline
value class Password(val value: String)

val pwd = Password("s3cr3t")
println(pwd)  // 输出: Password(value=s3cr3t) —— 泄漏密码!

// 正确做法:重写 toString
@JvmInline
value class Password(val value: String) {
    override fun toString() = "Password(***)"
}

生产事故案例:某应用日志中打印了 ApiKey 实例,因未重写 toString,导致 API Key 明文写入日志文件。事后审计发现日志已被备份至云端,被迫吊销并轮换所有 API Key。

陷阱 3:value class 作为可空参数

@JvmInline
value class UserId(val value: String)

// 反模式:可空参数导致每次调用都 boxing
fun findUser(id: UserId?): User? {
    return if (id != null) User(id.value) else null
}

// 改进:用 String? 替代,或拆分两个函数
fun findUser(id: UserId): User? = User(id.value)
fun findUserNullable(id: UserId?): User? = id?.let { User(it.value) }

陷阱 4:与 Java 代码互操作时的 boxing

@JvmInline
value class UserId(val value: String)

// Kotlin 代码
fun getUserId(): UserId = UserId("user_001")

// Java 调用方看到的签名:
// public static String getUserId()  -- 内联,但返回 String 而非 UserId
// 这导致 Java 调用方失去类型安全

生产事故案例:某 Java 老代码调用 Kotlin 新代码时,发现返回值是 String 而非 UserId,导致后续调用 findUser(String) 时编译通过但语义错误。修复方案:在 Kotlin 中显式暴露 boxed 类型给 Java。

陷阱 5:误用 value class 表达复杂领域模型

// 反模式:用 value class 表达复杂模型
@JvmInline
value class User(val value: String)  // 用 JSON 字符串存储用户对象?!

// 正确做法:用 data class
data class User(val id: String, val name: String, val email: String)

value class 适合单一语义值(ID、单位、密码),不适合多属性领域模型。

陷阱 6:在反序列化时绕过 init 验证

@JvmInline
value class Email(val value: String) {
    init {
        require(value.contains("@")) { "Invalid email" }
    }
}

// 反序列化框架可能绕过 init 块
val email = Json.decodeFromString<Email>("\"invalid-email\"")  // 可能不抛异常

修复方案:使用支持 value class init 验证的序列化框架,或在反序列化后显式验证。

工程实践

实践 1:领域 ID 系统化设计

package fandex.valueclass.ids

/**
 * 领域 ID 系统化设计:所有实体 ID 用 value class 包装。
 * 
 * 优势:
 * 1. 编译期类型安全,避免 ID 混用
 * 2. 运行期零开销
 * 3. 集中实现验证逻辑
 */

// 基础 ID 接口
interface Id<T> {
    val value: T
}

@JvmInline
value class UserId(override val value: String) : Id<String>

@JvmInline
value class OrderId(override val value: String) : Id<String>

@JvmInline
value class ProductId(override val value: String) : Id<String>

@JvmInline
value class AccountId(override val value: Long) : Id<Long>

// 服务层:参数类型严格
class OrderService {
    fun findOrder(id: OrderId): Order? = Order(id, UserId("u1"), 100.0)
    fun findUserOrders(userId: UserId): List<Order> = listOf()
    fun updateOrder(id: OrderId, modifier: UserId) { /* ... */ }
}

data class Order(val id: OrderId, val userId: UserId, val amount: Double)

fun main() {
    val service = OrderService()
    
    val userId = UserId("user_001")
    val orderId = OrderId("order_001")
    
    // 类型安全:编译器保证不会传错
    val order = service.findOrder(orderId)
    val userOrders = service.findUserOrders(userId)
    service.updateOrder(orderId, userId)
    
    // service.findOrder(userId)  // 编译错误
}

实践 2:与 JPA/Hibernate 集成

package fandex.valueclass.jpa

import javax.persistence.*
import kotlin.reflect.KProperty1

/**
 * value class 与 JPA 集成:自定义类型转换器。
 */

@JvmInline
value class UserId(val value: String)

@JvmInline
value class Email(val value: String)

@Entity
@Table(name = "users")
class UserEntity(
    @Id
    @Column(name = "id")
    var id: String,  // 数据库层仍是 String
    
    @Column(name = "email")
    var email: String,
    
    @Column(name = "name")
    var name: String
) {
    // 领域层暴露 value class
    fun toDomain() = User(
        id = UserId(id),
        email = Email(email),
        name = name
    )
    
    companion object {
        fun fromDomain(user: User) = UserEntity(
            id = user.id.value,
            email = user.email.value,
            name = user.name
        )
    }
}

data class User(
    val id: UserId,
    val email: Email,
    val name: String
)

// 使用 JPA AttributeConverter(更高级的集成)
@Converter(autoApply = false)
class UserIdConverter : AttributeConverter<UserId, String> {
    override fun convertToDatabaseColumn(attribute: UserId?): String? {
        return attribute?.value
    }
    
    override fun convertToEntityAttribute(dbData: String?): UserId? {
        return dbData?.let { UserId(it) }
    }
}

@Entity
class OrderEntity(
    @Id
    @Convert(converter = UserIdConverter::class)
    val userId: UserId  // 直接使用 value class
)

实践 3:性能基准测试

package fandex.valueclass.benchmark

import kotlinx.benchmark.*
import kotlin.random.Random

@JvmInline
value class Distance(val value: Double)

class DistanceWrapper(val value: Double)

@State(Scope.Benchmark)
class ValueClassBenchmark {
    @Param("1000", "10000", "100000")
    var size: Int = 0
    
    @Benchmark
    fun baselinePrimitive(bh: Blackhole) {
        var sum = 0.0
        repeat(size) {
            sum += it.toDouble()
        }
        bh.consume(sum)
    }
    
    @Benchmark
    fun valueClass(bh: Blackhole) {
        var sum = Distance(0.0)
        repeat(size) {
            sum = Distance(sum.value + it)
        }
        bh.consume(sum)
    }
    
    @Benchmark
    fun wrapperClass(bh: Blackhole) {
        var sum = DistanceWrapper(0.0)
        repeat(size) {
            sum = DistanceWrapper(sum.value + it)
        }
        bh.consume(sum)
    }
    
    @Benchmark
    fun valueClassInList(bh: Blackhole) {
        val list = List(size) { Distance(it.toDouble()) }
        var sum = 0.0
        list.forEach { sum += it.value }
        bh.consume(sum)
    }
    
    @Benchmark
    fun wrapperClassInList(bh: Blackhole) {
        val list = List(size) { DistanceWrapper(it.toDouble()) }
        var sum = 0.0
        list.forEach { sum += it.value }
        bh.consume(sum)
    }
}

实践 4:Android 中的内存优化

package fandex.valueclass.android

import android.os.Parcelable
import android.os.Parcel

/**
 * Android 中使用 value class 优化内存。
 * 
 * 注意:value class 实现 Parcelable 需要装箱。
 */

@JvmInline
value class UserId(val value: String)

@JvmInline
value class Coordinate(val value: Float) {
    operator fun plus(other: Coordinate) = Coordinate(value + other.value)
}

// 大量坐标点的场景(如地图绘制)
class MapRenderer {
    // 使用 FloatArray 而非 Array<Coordinate>,避免 boxing
    private val coordinates = FloatArray(10000) { it.toFloat() }
    
    fun render() {
        // 内部用 FloatArray,对外暴露 Coordinate
        for (i in coordinates.indices step 2) {
            val x = Coordinate(coordinates[i])
            val y = Coordinate(coordinates[i + 1])
            drawPoint(x, y)
        }
    }
    
    private fun drawPoint(x: Coordinate, y: Coordinate) {
        // 绘制逻辑
    }
}

案例研究

案例 1:某支付系统金额处理

业务场景:支付系统涉及多种货币、多种金额表示(分、元、比特币单位 satoshi),需要严格区分避免混淆。

实现:

@JvmInline
value class Cents(val value: Long) {
    operator fun plus(other: Cents) = Cents(value + other.value)
    operator fun minus(other: Cents) = Cents(value - other.value)
    operator fun times(factor: Int) = Cents(value * factor)
    operator fun compareTo(other: Cents) = value.compareTo(other.value)
    
    fun toYuan() = value / 100.0
    companion object { val ZERO = Cents(0) }
}

@JvmInline
value class Satoshi(val value: Long) {
    operator fun plus(other: Satoshi) = Satoshi(value + other.value)
    fun toBtc() = value / 100_000_000.0
}

@JvmInline
value class Currency(val code: String) {
    init {
        require(code.length == 3 && code.all { it.isUpperCase() }) { "Invalid currency" }
    }
}

class PaymentService {
    fun transfer(amount: Cents, currency: Currency, from: String, to: String): Boolean {
        require(amount > Cents.ZERO) { "金额必须大于 0" }
        println("Transfer ${amount.toYuan()} ${currency.code} from $from to $to")
        return true
    }
    
    fun convertToBtc(usdAmount: Cents, rate: Double): Satoshi {
        val btcValue = usdAmount.toYuan() / rate
        return Satoshi((btcValue * 100_000_000).toLong())
    }
}

fun main() {
    val service = PaymentService()
    val usd = Currency("USD")
    val amount = Cents(1099)  // $10.99
    
    service.transfer(amount, usd, "alice", "bob")
    
    val btcAmount = service.convertToBtc(amount, 50000.0)
    println("BTC: ${btcAmount.toBtc()}")
}

效果:上线后未再发生金额单位混淆导致的资金事故。代码相比 Java 包装类版本内存占用降低 60%。

案例 2:某 SaaS 平台多租户 ID 隔离

业务场景:SaaS 平台需要严格隔离不同租户的数据,所有数据库查询都必须带租户 ID。

实现:

@JvmInline
value class TenantId(val value: String) {
    init {
        require(value.startsWith("tnt_")) { "Invalid tenant ID" }
    }
}

@JvmInline
value class ResourceId(val value: String)

interface TenantScoped {
    val tenantId: TenantId
}

data class Document(
    override val tenantId: TenantId,
    val id: ResourceId,
    val content: String
) : TenantScoped

class DocumentRepository {
    fun find(tenant: TenantId, id: ResourceId): Document? {
        // 编译期保证:必须传 TenantId,不能用 ResourceId 替代
        return Document(tenant, id, "content")
    }
    
    fun listByTenant(tenant: TenantId): List<Document> {
        return listOf()
    }
}

fun main() {
    val repo = DocumentRepository()
    val tenant = TenantId("tnt_001")
    val doc = ResourceId("doc_001")
    
    val document = repo.find(tenant, doc)
    println("Found: $document")
    
    // repo.find(doc, tenant)  // 编译错误,避免租户 ID 与资源 ID 混淆
}

效果:彻底消除了租户数据越权访问的 bug,相比原 String ID 方案,未增加任何运行时开销。

案例 3:某 IoT 平台传感器数据

业务场景:IoT 平台接收来自不同传感器的数据,单位多样(温度、湿度、气压等),需要严格区分。

实现:

@JvmInline
value class Celsius(val value: Double) {
    fun toFahrenheit() = Fahrenheit(value * 9 / 5 + 32)
    operator fun plus(other: Celsius) = Celsius(value + other.value)
}

@JvmInline
value class Fahrenheit(val value: Double) {
    fun toCelsius() = Celsius((value - 32) * 5 / 9)
}

@JvmInline
value class Humidity(val percent: Double) {
    init {
        require(percent in 0.0..100.0) { "湿度必须在 0-100 之间" }
    }
}

@JvmInline
value class Hectopascal(val value: Double)

sealed class SensorReading {
    abstract val sensorId: String
    abstract val timestamp: Long
    
    data class Temperature(
        override val sensorId: String,
        override val timestamp: Long,
        val value: Celsius
    ) : SensorReading()
    
    data class HumidityReading(
        override val sensorId: String,
        override val timestamp: Long,
        val value: Humidity
    ) : SensorReading()
    
    data class PressureReading(
        override val sensorId: String,
        override val timestamp: Long,
        val value: Hectopascal
    ) : SensorReading()
}

class SensorProcessor {
    fun process(readings: List<SensorReading>): Map<String, Double> {
        val stats = mutableMapOf<String, Double>()
        readings.forEach { reading ->
            when (reading) {
                is SensorReading.Temperature -> {
                    val key = "temp_${reading.sensorId}"
                    stats[key] = (stats[key] ?: 0.0) + reading.value.value
                }
                is SensorReading.HumidityReading -> {
                    val key = "humidity_${reading.sensorId}"
                    stats[key] = (stats[key] ?: 0.0) + reading.value.percent
                }
                is SensorReading.PressureReading -> {
                    val key = "pressure_${reading.sensorId}"
                    stats[key] = (stats[key] ?: 0.0) + reading.value.value
                }
            }
        }
        return stats
    }
}

基础题

习题 1:以下代码是否能编译通过?说明原因。

@JvmInline
value class Container(val a: Int, val b: Int)

参考答案要点:

  • 不能编译通过
  • 原因:value class 只能有一个底层属性
  • 修复:
// 方案 1:使用 data class
data class Container(val a: Int, val b: Int)

// 方案 2:用 String 编码
@JvmInline
value class Container(val encoded: String) {
    val a: Int get() = encoded.substringBefore(",").toInt()
    val b: Int get() = encoded.substringAfter(",").toInt()
}

习题 2:以下场景中,value class 是否会发生 boxing?

@JvmInline
value class UserId(val value: String)

fun findUser(id: UserId): User?
fun findUserNullable(id: UserId?): User?
val users: List<UserId> = listOf()
val user: Any = UserId("u1")

参考答案要点:

  • findUser(id: UserId):不发生 boxing(参数非空)
  • findUserNullable(id: UserId?):发生 boxing(可空类型)
  • List<UserId>:发生 boxing(泛型)
  • Any:发生 boxing(装箱到 Object)

进阶题

习题 3:实现一个 Meter value class,支持与 Kilometer 互转,并实现 Comparable<Meter> 接口。

参考答案要点:

@JvmInline
value class Meter(val value: Double) : Comparable<Meter> {
    override fun compareTo(other: Meter): Int = value.compareTo(other.value)
    
    operator fun plus(other: Meter) = Meter(value + other.value)
    operator fun minus(other: Meter) = Meter(value - other.value)
    
    fun toKilometer() = Kilometer(value / 1000)
}

@JvmInline
value class Kilometer(val value: Double) {
    fun toMeter() = Meter(value * 1000)
}

习题 4:分析以下代码的内存占用差异。

// 方案 A
val ids1: List<String> = (1..1000000).map { "id_$it" }

// 方案 B
@JvmInline
value class Id(val value: String)
val ids2: List<Id> = (1..1000000).map { Id("id_$it") }

参考答案要点:

  • 方案 A:100 万个 String 引用,约 8MB(仅引用,不含 String 对象本身)
  • 方案 B:100 万个 Id 包装对象,每个 24 字节,约 24MB
  • 原因:List<Id> 中每个 Id 都需要 boxing
  • 结论:在集合场景下,value class 反而增加内存

挑战题

习题 5:设计一个跨平台(JVM + JS + Native)的 Money 类,要求:

  1. 用 value class 表示金额(以分为单位)
  2. 支持加减乘除
  3. 支持 JSON 序列化
  4. 跨平台兼容

参考答案要点:

// commonMain
expect annotation class JvmInline()

@JvmInline
value class Money(val cents: Long) : Comparable<Money> {
    override fun compareTo(other: Money): Int = cents.compareTo(other.cents)
    
    operator fun plus(other: Money) = Money(cents + other.cents)
    operator fun minus(other: Money) = Money(cents - other.cents)
    operator fun times(factor: Int) = Money(cents * factor)
    operator fun div(divisor: Int) = Money(cents / divisor)
    
    val dollars: Double get() = cents / 100.0
}

// 各平台实现 expect
// jvmMain: actual annotation class JvmInline() { ... }

习题 6:分析 value class 在 Kotlin/Native 上的实现差异。

参考答案要点:

  • Kotlin/Native 不需要 @JvmInline 注解
  • Native 平台 value class 通过编译器直接内联实现
  • Native 平台对 value class 的 boxing 时机与 JVM 略有不同(取决于运行时表示)
  • 跨平台代码中应使用 expect/actual 处理平台差异

官方文档

经典教材

  • 《Kotlin in Action》(Dmitry Jemerov、Svetlana Isakova 著):扩展函数与内联机制
  • 《Programming in Scala》(Martin Odersky 著):Scala value class 设计哲学
  • 《Real-World Haskell》(Bryan O’Sullivan 著):newtype 模式的经典介绍

前沿论文

  • KEEP-267: Inline Classes(Roman Elizarov, 2018):value class 的设计动机
  • Compiling with Continuations(Andrew Appel, 1992):内联优化的理论基础
  • The Newtype Pattern in Haskell(Mark Jones, 1995):newtype 的早期形式化

开源项目

定义内联类

基本写法:定义 value class @JvmInline value class <类名>(val <属性>: <类型>)

// 定义内联值类避免装箱开销
@JvmInline
value class UserId(val value: Long)

基本写法:字符串包装类 @JvmInline value class <类名>(val <属性>: String)

// 包装字符串为强类型
@JvmInline
value class Email(val value: String)

使用内联类

基本写法:创建实例 <类名>(<值>)

// 创建内联类实例
val id = UserId(42L)

基本写法:访问底层值 <实例>.<属性>

// 取出底层值
val raw: Long = id.value

基本写法:作为参数 fun <方法>(<参数>: <内联类>) {}

// 函数签名使用强类型
fun find(id: UserId) = repo.findById(id.value)

内联类方法

基本写法:定义方法 @JvmInline value class <类>(val v: <类型>) { fun <方法>() {} }

// 内联类可定义方法
@JvmInline
value class Meter(val value: Double) {
    fun toKm() = Kilometer(value / 1000)
}

基本写法:扩展函数 fun <内联类>.<方法>() {}

// 为内联类添加扩展
fun Meter.toCm() = value * 100

内联类与接口

基本写法:实现接口 @JvmInline value class <类>(val v: <类型>) : <接口> {}

// 内联类实现接口
interface Printable { fun print() }
@JvmInline
value class Name(val value: String) : Printable {
    override fun print() = println(value)
}

泛型与内联类

基本写法:泛型内联类 @JvmInline value class <类><<T>>(val v: <T>)

// 泛型内联类
@JvmInline
value class Box<T>(val value: T)

nullable 内联类

基本写法:可空类型字段 @JvmInline value class <类>(val v: <类型>?)

// 内部可空时装箱为包装类
@JvmInline
value class Phone(val value: String?)

underlying 字段类型

基本写法:数字底层类型 @JvmInline value class <类>(val v: <数字类型>)

// 底层为 Int 编译期不装箱
@JvmInline
value class Age(val value: Int)

与 Java 互操作

基本写法:Java 调用需传底层类型 <Java 方法>(<底层值>);

// Java 中调用需传 Long 而非 UserId
// Kotlin 端添加 @JvmName 避免签名冲突
@JvmInline
value class UserId(val value: Long) {
    @JvmName("of") companion object { fun of(v: Long) = UserId(v) }
}

value class 与 data class 区别

基本写法:何时用 value class @JvmInline value class <类>(val v: <单字段>)

// 单字段且无需 equals 时使用 value class
@JvmInline value class Token(val value: String)

基本写法:何时用 data class data class <类>(val <字段1>, val <字段2>)

// 多字段或需 equals 时使用 data class
data class Point(val x: Int, val y: Int)

内联类集合

基本写法:内联类作为 Map 键 Map<<内联类>, <值>>

// 内联类作为 Map 键避免装箱
val map: Map<UserId, String> = mapOf(UserId(1) to "Alice")

Sealed 结合内联类

基本写法:内联类配合密封类 sealed class <基> | @JvmInline value class <子>: <基>

// 内联类作为密封类子类
sealed interface Id
@JvmInline value class LongId(val v: Long) : Id
@JvmInline value class Uuid(val v: String) : Id

验证构造

基本写法:init 校验 @JvmInline value class <类>(val v: <类型>) { init { require(<条件>) } }

// 构造时校验合法性
@JvmInline
value class NonBlank(val value: String) {
    init { require(value.isNotBlank()) { "blank" } }
}

性能优化场景

基本写法:ID 类型避免装箱 @JvmInline value class <Id>(val v: <Long>)

// 大量 ID 时用内联类避免 Long 装箱
@JvmInline value class OrderId(val v: Long)
val ids = listOf(OrderId(1), OrderId(2)) // 数组无装箱

toString 与 equals

基本写法:自定义 toString override fun toString() = "<格式>"

// 自定义字符串表示
@JvmInline
value class UserId(val value: Long) {
    override fun toString() = "User($value)"
}

内联类限制

基本写法:只能有单构造属性 @JvmInline value class <类>(val <属性>: <类型>)

// 仅支持单个 val 属性作为底层值
@JvmInline value class Score(val value: Int)

基本写法:内部不能有可变状态 @JvmInline value class <类>(val v: <类型>) { var <变量> } // 禁止

// 内联类不能有可变 var 属性
@JvmInline value class Score(val value: Int)