原文链接:curiouslab.dev/0002-value-…
近日,Valhalla团队发布了一个实现值类功能首阶段的早期JDK访问版本。更多详情及试用指南请参见此处。尽管该功能尚需时间完善,但这对JVM生态系统而言无疑是重大利好!这也为我们提供了绝佳契机,深入探讨Kotlin自身在值类、可变性/不可变性方面的规划,以及这些特性与数据类之间的关联。
数据类、记录、案例类?产品类型!
从高级编程语言的"早期"阶段开始,就存在许多相似的概念:记录(最早出现在ALGOL和Pascal中)、结构体、命名元组、案例类、Kotlin中的数据类等等。它们都服务于同一个基本目的:将可能异构的数据片段组合成单一单元。根据目标平台的具体特性,它们可能附带额外语义,也可能没有。但首要功能始终是承载数据。
理论上,这都与所谓的"产品类型"有关,因为组合类型(无论具体形式)能够容纳其值的笛卡尔积:|X| × |Y|。而这里出现的值类,顾名思义同样围绕"值"展开,同样属于产品类型。它们主要用于存储数据,但其语义与Kotlin现有构造存在显著差异。
让我们深入剖析,看看它为何能成为语言的绝佳补充。
Value类. 构建模块.
简而言之,我们计划为Kotlin引入值类。它们在某种程度上类似于Java中的值类——同样不具备身份(毕竟是值类),但设计上并非完全复刻。接下来我将阐述其构建模块,并讲述值类变异的故事,这在Java视角下显得尤为突出。
解构
还记得我们讨论过基于名称的解构吗?我们提到现有数据类存在复用componet运算符函数问题,这在语义上并不理想,还会增加ABI接口。好消息是值类作为新型类,让我们能直接采用基于名称的解构设计,从而避免添加这些组件函数:
value class Money(val amount: BigDecimal, val currency: String)
val (amount, currency) = money // good!
val (currency) = money // good! Take only currency
val (amount, country) = money // error! There's no "country" property
equals, hashCode 和 toString
位置解构或许并不出色,但Any自动生成的方法却颇为实用。我们将生成equals和hashCode的结构化实现,以及一个漂亮的toString方法:
value class Money(val amount: BigDecimal, val currency: String)
println(money) // prints "Money(amount=10.0, currency=USD)"
Value基类,浅层不可变性
值类没有身份属性,它们完全由其底层数据表示。因此,===(身份相等)运算符对它们而言毫无意义。基于相同原因,可变性也不适用于值类。在Kotlin中,这意味着值类只能拥有val属性:
value class Money(val amount: BigDecimal, val currency: String) // good!
value class Book(val name: String, var author: Author) // error! "author" cannot be var
(不)变性。复制var
因此,值类具有浅层不可变性(以当前Kotlin的术语而言,只能包含val)。要修改值类的值,我们需要调用构造函数、调用复制函数、使用with语句或采取类似操作。请注意此操作是安全的,因为我们只是创建新副本,不会影响程序的当前状态。
另一方面,引用类型的修改只需使用=运算符即可实现,且具有破坏性——它会实际改变状态。对比:
value class ValuePostcode(val code: String)
class RefPostcode(var code: String)
var valPostcode = ValuePostcode("1021ab")
val refPostcode = RefPostcode("1021ab")
// Safe operation. Done with constructor, copy, wither
valPostcode = ValuePostcode(valPostcode.code.uppercase())
// Potentially unsafe, references to refPostcode also affected. Shortly done with "="
refPostcode.code = refPostcode.code.uppercase()
关键点在于:优秀的系统设计告诉我们,抽象机制应当让"正确操作"变得简单,同时阻止"错误操作"。然而对于普通值类而言,使用引用时调整值反而变得更容易——尽管这可能引发问题!
理智上我们或许明白,在需要安全"变异"的场景中值类更优。但实践中,若引用操作更便捷,多数人仍会选择这条路——这完全源于抽象机制的设计缺陷。正因如此,我们才要尝试"复制变量"方案:既能享受引用变异的便利,又能保留值类的安全性:
value class ValuePostcode(copy var code: String) // note "copy" here
var valPostcode = ValuePostcode("1021ab")
// Safe operation. Under the hood, we created a copy. On the language level, it's just "="
valPostcode.code = valPostcode.code.uppercase()
// Under the hood, semantically, it's still the same as invoking a constructor
// valPostcode = ValuePostcode(valPostcode.code.uppercase())
这样一来,修改值类就变得和操作引用一样简单。由于值类现在提供了更多优势,同时避免了可变引用的弊端,它们真正能成为日常工具,引领我们构建更安全的系统!
运行时性能与Valhalla项目
好的。到目前为止,我们主要讨论了值类的语义特性:它们如何支持基于名称的解构,提供便捷的equals、hashCode和toString实现,以及通过"copy vars"实现安全变异的语言支持。
但运行时性能如何呢?
在Kotlin/JVM环境中,这取决于JVM。Kotlin中的值类设计旨在便于JVM优化,但其首要目标是语义实现:我们并未专门为性能而设计。得益于Project Valhalla,我们能在JVM上使用相同的类型描述符,自动获得标量化和堆扁平化优化。更重要的是,内联函数将使我们能够编写泛型代码来操作值类,并从中受益于标量化优化!
与此同时,这也意味着Kotlin并不依赖于Valhalla甚至JVM本身。无论Java或JVM如何演变,我们都能独立交付值类功能。实际应用中,这意味着即使您针对的是不支持值类的JVM,只要使用现代Kotlin版本,仍可充分利用值类及其语言级语义。此外,若目标是支持值类的现代JVM,你还将获得运行时性能提升。
不过值类的运行时性能可能较为复杂,它取决于值类的大小、完整性要求、可空性及堆扁平化等因素。因此我们希望将运行时性能的责任交给JVM,避免将其纳入语言设计本身。
数据类?
再次强调:值类从诞生之初就具备基于名称的解构能力(ABI中不存在组件),拥有美观的toString和equals方法以及hashCode。它们在可变性方面更安全,在面向现代JVM时还能获得运行时性能优势。
此时你或许会想:"不错,但这不就是数据类,只是...更优秀吗?"没错。
最终,值类将在Kotlin中日益普及。数据类仍将保留——我们不会弃用它们,当领域中需要引用语义时它们依然有用——但其使用频率会自然下降。
简而言之:若需在数据类与值类间抉择,请优先选择值类!
实用链接
KotlinConf'25 演讲:深入浅出不可变性:Valhalla 及未来 | Marat Akhin
JVMLS'25 演讲:借助 Valhalla 实现更优的 Kotlin 不可变性