Kotlin的值类(Value Class)是一种强大的类型安全工具,允许开发者创建语义明确的类型,并保持运行时零成本。

假设系统中存在用户的概念,用户拥有名字和电子邮箱地址。用户名和电子邮箱地址都是长度不超过120个字符的字符串。用户名不能是空白,不能是"null",也不能包含"@"。电子邮箱地址必须包含"@"。根据这些要求,我们可以得到一个简单的模型。

代码1  简单的User模型

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

这个模型没有对值进行校验,客户端代码可能直接调用 user.name = "null" ,产生一条不满足业务约束的数据。为了避免这种情况,我们可以为用户名、电子邮箱分别建立模型。

代码2  复杂的User模型

data class User(val name: UserName, val email: Email)

class UserName(val value: String) {
    init {
        require(!value.contains("@") && ... ) { "Invalid userName" }
    }
}

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

复杂模型可以保证业务逻辑不出错,但多了一个包装对象,产生运行时性能损耗。仔细观察UserName和Email两个类,都是把一个String对象和一些专属操作绑定起来,构成一个新类型。新类型可以表达语义,操作可以校验值。这两样都是我们需要的。有没有办法既能做到这两点,又不会产生额外的包装对象呢?答案就是值类。

代码3  值类:“基础”类型零成本抽象

@JvmInline
value class UserName(val value: String) {
    init {
      require(!value.contains("@") && ... ) { "Invalid userName" }
    }
  }

从语法上看,值类版本的UserName只比普通版本多了 @JvmInline 注释,并且将 class 换成了 value class ,其他方面并无差别。但在运行时,值类不会产生额外性能损耗,可以做到零成本抽象。

值类能做到运行时零成本的方法和C++模板或TypeScript类似,编译时在字节码级别进行内联。比如下面的值类

@JvmInline
value class Meter(val value: Double)

fun calculate(m: Meter) = m.value * 2

编译后的字节码等价于

public static double calculate(double m) {
    return m * 2;
}

因此值类可以做到:

  • 没有额外对象分配
  • 没有虚方法表
  • 没有对象头开销
  • 方法调用转为静态分派

当然值类的使用也存在一些限制,包括:

  • 不能声明多个属性
  • 不能继承其他类(可以实现接口)
  • 不能在反射场景中使用
  • 需要特殊处理泛型场景

JVM泛型需要对象,因此在泛型中使用值类会引发装箱。

// 触发装箱
val list = listOf(UserId("123")) 

// 方案1:使用原始类型数组避免装箱(推荐)
val array = arrayOf(UserId("123"))

// 方案2:通过inline class+类型投影减少装箱
val list = listOf<UserId>(UserId("123"))

值类的使用场景有:

  • 需要区分语义相似的原始类型时 (名字, 邮件等)
  • 需要为简单值添加领域行为时
  • 高频调用的基础类型包装
  • 要求极致性能的数值计算场景
  • 大型项目中的领域模型定义

需要避免值类的场景有:

  • 需要包装多个字段的复杂对象
  • 需要复杂继承关系的类型
  • 深度依赖反射的操作
  • 与某些Java框架深度集成的场景

值类的核心优势在于:

  1. 编译时类型安全
  2. 领域语义明确
  3. 零成本抽象
  4. 减少模型转换样板
  5. 增强代码可读性和可维护性
表1  值类和数据类对比
特性 值类 数据类(Data Class)
内存开销 零(运行时内联) 每个对象额外16-24字节对象头
适用场景 单值包装 多属性数据容器(如DTO)
自动生成方法 仅基于包装值的方法 equals()/hashCode()/copy()等
泛型处理 可能触发装箱 直接支持
表2  值类与装箱对比
特性 值类 装箱(以Integer为例)
设计目标 类型安全的语义增强 原始类型与对象类型的转换桥梁
内存开销 0 (编译时内联) Integer: 16+字节对象头
类型系统 创建真正的新类型 int和Integer是相同值的不同表示
空值安全 默认非空 (显式声明可空) int不能null, Integer可为null
集合性能 等同于原始类型集合 对象指针集合 (内存碎片化)
使用场景 领域建模中的语义化类型 泛型兼容和对象类型需求
表3  值类与值对象对比
维度 值类 DDD值对象 (Value Object)
范畴 编程语言特性 (Kotlin特有) 领域驱动设计(DDD)概念
核心目的 零开销的类型安全包装 表示没有唯一标识的领域概念
实现方式 @JvmInline value class 不可变类(通常用 data class)
身份标识 无明确要求 无唯一标识 (靠属性值区分)
相等性 基于包装值 (可自定义) 基于所有属性值
可变性 默认可变 (但通常设计为不可变) 严格不可变
典型应用 ID包装、单位封装、类型别名 金额、地址、日期范围、坐标点

值对象(Value Object)是领域驱动设计中不可变的概念片段,值类是Kotlin零开销的类型安全包装特性。二者主要是名称相似。如果当值对象只需封装单个值时,值类是最佳实现方式。

表4  值类和扩展方法
维度 值类 扩展方法
本质 创建新类型 扩展现有类型
类型系统 编译时引入新类型 (运行时内联) 不引入新类型
作用范围 全局性的类型安全增强 局部性的功能增强
主要目的 解决类型安全问题 解决功能扩展问题
使用方式 创建新类型实例 在现有类型实例上调用
性能影响 零运行时开销 极低开销(静态方法调用)
领域建模 核心领域概念建模 辅助功能实现

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐