kotlin-types-value-class
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseKotlin value class vs data class
Kotlin value class 与 data class 对比
Core principle
核心原则
Prefer for single-field types that carry domain meaning. Data classes are for aggregating multiple fields. A value class gives you type safety (you can't mix up and ) without the allocation overhead of a data class.
@JvmInline value classUserIdString对于具有业务领域含义的单字段类型,优先使用。data class适用于聚合多个字段的场景。value class能为你提供类型安全性(不会混淆和),同时避免data class带来的内存分配开销。
@JvmInline value classUserIdStringWhen to use this skill
适用场景
- Writing a new Kotlin type that wraps a single value
- Reviewing a data class that has only one property
- Seeing primitive types (,
String,Long, etc.) used where a domain type would prevent misuseInt - Compose compiler reports showing unstable parameters that could be value classes
- 编写包装单一值的新Kotlin类型时
- 审查仅包含一个属性的data class时
- 发现原始类型(、
String、Long等)被用于可通过领域类型避免误用的场景时Int - Compose编译器报告显示存在可改为value class的不稳定参数时
Decision flow
决策流程
| Situation | Prefer |
|---|---|
Single field + domain-meaningful ( | |
| Single field + no domain meaning (just grouping) | Type alias or keep the primitive |
| Multiple fields | Data class |
Needs custom | Data class (value classes delegate to the underlying type) |
| Used as a generic type argument or nullable in hot paths | Data class or primitive (autoboxing cost) |
kotlin
// GOOD: domain-meaningful single field
@JvmInline value class UserId(val value: String)
@JvmInline value class EmailAddress(val value: String)
@JvmInline value class Percentage(val value: Float)
// BAD: data class wrapping a single field
data class UserId(val value: String) // unnecessary allocation
data class EmailAddress(val value: String) // type safety without the overhead is available
// BAD: value class with no domain meaning
@JvmInline value class Wrapper(val value: String) // just use the String, or a type alias
// BAD: value class needing custom equality
@JvmInline value class CaseInsensitiveString(val value: String)
// value class equals delegates to String equals, which IS case-sensitive
// Use a data class if you need different equality semantics| 场景 | 优先选择 |
|---|---|
单字段且具有业务领域含义(如 | |
| 单字段但无业务领域含义(仅用于分组) | 类型别名(Type alias)或直接保留原始类型 |
| 多字段 | data class |
需要在包装值之外自定义 | data class(value class的相关方法会委托给底层类型) |
| 在性能敏感路径中用作泛型类型参数或可空类型 | data class或原始类型(避免自动装箱开销) |
kotlin
// GOOD: domain-meaningful single field
@JvmInline value class UserId(val value: String)
@JvmInline value class EmailAddress(val value: String)
@JvmInline value class Percentage(val value: Float)
// BAD: data class wrapping a single field
data class UserId(val value: String) // unnecessary allocation
data class EmailAddress(val value: String) // type safety without the overhead is available
// BAD: value class with no domain meaning
@JvmInline value class Wrapper(val value: String) // just use the String, or a type alias
// BAD: value class needing custom equality
@JvmInline value class CaseInsensitiveString(val value: String)
// value class equals delegates to String equals, which IS case-sensitive
// Use a data class if you need different equality semanticsCompose stability
Compose 稳定性
@JvmInline value classStableString- Value classes passed as composable parameters avoid "unstable parameter" warnings
- No need for annotations at Compose boundaries when wrapping primitives or strings
@Immutable - Replacing single-field data classes with value classes at UI boundaries improves skippability
kotlin
// Before: data class wrapping a single field
data class UiState(val userId: String) // works, but allocates a wrapper object
// After: value class is stable and zero-allocation at runtime
@JvmInline value class UserId(val value: String)
data class UiState(val userId: UserId)当的底层类型为稳定类型(原始类型、及其他稳定类型)时,会被Compose编译器视为类型。这意味着:
@JvmInline value classStringStable- 作为可组合函数参数传递的value class可避免「不稳定参数」警告
- 包装原始类型或字符串时,无需在Compose边界添加注解
@Immutable - 在UI边界将单字段data class替换为value class可提升可跳过性(skippability)
kotlin
// Before: data class wrapping a single field
data class UiState(val userId: String) // works, but allocates a wrapper object
// After: value class is stable and zero-allocation at runtime
@JvmInline value class UserId(val value: String)
data class UiState(val userId: UserId)Gotchas
注意事项
- Autoboxing: Value classes are unboxed at compile time but boxed (allocated) when used as nullable (), generic type arguments (
UserId?), or vararg parameters. In hot paths these allocations matter; in most code they don't.List<UserId> - No backing fields: You cannot use blocks,
init, or delegated properties likelateinit. The class body is extremely constrained — only the single constructor parameter exists.by lazy - No data-class conveniences: No , no
copy()for destructuring, and no way to customizecomponent1(). If you need any of these, use a data class.toString() - No custom equals/hashCode/toString: These always delegate to the underlying type. Need custom equality → use a data class.
- when exhaustiveness: Sealed hierarchies of value classes work differently than data class hierarchies. Test branches carefully.
when - Serialization semantics: With kotlinx.serialization, a serializes as
@Serializable data class A(val value: String), but a{"value":"..."}serializes as the underlying value (@Serializable value class A(val value: String)). Replacing a single-field data class with a value class is a breaking change for your API/JSON contract."..." - Serialization: Some serialization frameworks need explicit support for value classes (e.g., kotlinx.serialization's works, but Jackson may need configuration).
@Serializable - Interoperability: From Java, value classes appear as their underlying type. Java callers bypass the type-safety wrapper.
- Reflection and runtime erasure: When passed as or used in generic contexts, value classes box into a synthetic wrapper class. Java reflection sees mangled method signatures, and frameworks that rely on raw runtime types (some ORMs, DI containers, or serializers) may see the underlying type rather than the value class.
Any
- 自动装箱:value class在编译时是未装箱的,但用作可空类型()、泛型类型参数(
UserId?)或可变参数时会被装箱(分配内存)。在性能敏感路径中这些分配会产生影响,但在大多数代码中无需在意。List<UserId> - 无后备字段:无法使用代码块、
init或lateinit这类委托属性。类体受到严格限制——仅存在单个构造函数参数。by lazy - 无data class便捷特性:没有方法,无法通过解构获取
copy(),也无法自定义component1()。如果需要这些特性,请使用data class。toString() - 无法自定义equals/hashCode/toString:这些方法始终委托给底层类型。需要自定义相等逻辑→使用data class。
- when表达式穷尽性:value class的密封层次结构与data class的层次结构表现不同。需仔细测试分支。
when - 序列化语义:使用kotlinx.serialization时,会序列化为
@Serializable data class A(val value: String),而{"value":"..."}会序列化为底层值(@Serializable value class A(val value: String))。将单字段data class替换为value class会导致API/JSON契约出现破坏性变更。"..." - 序列化支持:部分序列化框架需要显式支持value class(例如kotlinx.serialization的可正常工作,但Jackson可能需要额外配置)。
@Serializable - 互操作性:在Java中,value class会表现为其底层类型。Java调用者会绕过类型安全包装器。
- 反射与运行时擦除:当作为类型传递或在泛型上下文中使用时,value class会装箱为一个合成包装类。Java反射会看到被混淆的方法签名,依赖原始运行时类型的框架(部分ORM、DI容器或序列化器)可能会识别到底层类型而非value class。
Any
Packing multiple values
存储多个值
A value class can only declare one field, but Compose provides , , and matching functions in to store multiple primitives in a single . This lets you represent composite values (e.g., a 2D point, size, or padding) as a zero-allocation value class instead of a multi-field data class.
packFloatspackIntsunpack*androidx.compose.ui.utilLongkotlin
@JvmInline value class Offset(val packedValue: Long)
fun Offset(x: Float, y: Float): Offset = Offset(packFloats(x, y))
val Offset.x: Float get() = unpackFloat1(packedValue)
val Offset.y: Float get() = unpackFloat2(packedValue)- Only use this in performance-critical paths — manual bit-packing is error-prone. A data class is simpler and safer for most UI types.
- Available in —
androidx.compose.ui.util,packFloats,packInts,unpackFloat1,unpackFloat2,unpackInt1.unpackInt2
value class只能声明一个字段,但Compose在中提供了、及对应的函数,可将多个原始类型存储到单个中。这让你可以将复合值(如二维坐标、尺寸或内边距)表示为零分配的value class,而非多字段的data class。
androidx.compose.ui.utilpackFloatspackIntsunpack*Longkotlin
@JvmInline value class Offset(val packedValue: Long)
fun Offset(x: Float, y: Float): Offset = Offset(packFloats(x, y))
val Offset.x: Float get() = unpackFloat1(packedValue)
val Offset.y: Float get() = unpackFloat2(packedValue)- 仅在性能敏感路径中使用——手动位打包容易出错。对于大多数UI类型,data class更简单安全。
- 可在中获取——
androidx.compose.ui.util、packFloats、packInts、unpackFloat1、unpackFloat2、unpackInt1。unpackInt2
Common mistakes
常见错误
| Mistake | Fix |
|---|---|
| Data class wrapping a single domain field | Replace with |
| Value class with no domain meaning (just a wrapper) | Use a type alias or the primitive directly |
| Value class needing custom equality | Use a data class instead |
| Value class as generic type argument in hot path | Accept autoboxing cost or use the primitive |
| Replace with value class — it's Stable by default |
Forgetting | Always pair |
| 错误 | 修复方案 |
|---|---|
| 使用data class包装单一业务领域字段 | 替换为 |
| 使用无业务领域含义的value class(仅作为包装器) | 使用类型别名或直接使用原始类型 |
| 使用需要自定义相等逻辑的value class | 改用data class |
| 在性能敏感路径中将value class用作泛型类型参数 | 接受自动装箱开销或改用原始类型 |
在可改为value class的类型上添加 | 替换为value class——它默认是Stable类型 |
忘记添加 | 单字段类使用 |
Red flags during review
代码审查中的警示信号
- A data class with exactly one property
- A ,
String, orLongused where different values should not be interchangeable (e.g.,Int)fun transfer(from: String, to: String, amount: Long) - An annotation on a single-field wrapper
@Immutable - A type alias used for domain distinction where value-class semantics are needed (type aliases are type-erased, no runtime protection)
- 仅包含一个属性的data class
- 在不应互换不同值的场景中使用、
String或Long(例如Int)fun transfer(from: String, to: String, amount: Long) - 在单字段包装类上添加注解
@Immutable - 使用类型别名实现领域区分,但实际需要value class语义(类型别名会被类型擦除,无运行时保护)
When NOT to apply
不适用于以下场景
- The type needs multiple fields → data class
- The type needs custom /
equals/hashCode→ data classtoString - The type is used heavily as a nullable or generic in performance-critical code → measure autoboxing cost first
- The project does not need the type-safety distinction → a type alias or primitive is sufficient
- 类型需要多个字段→使用data class
- 类型需要自定义/
equals/hashCode→使用data classtoString - 类型在性能敏感代码中频繁用作可空类型或泛型→先评估自动装箱开销
- 项目不需要类型安全区分→使用类型别名或原始类型即可
Related
相关链接
- — diagnose unstable Compose parameters; value classes are one fix
compose-stability-diagnostics
- —— 诊断Compose不稳定参数;value class是解决方案之一
compose-stability-diagnostics