简介
Kotlin 早在 1.7.20版本就引入了一种新的对象声明类型:data object
,但是处于Experimental
阶段 。data object
与常规object
在概念上表现一致,但带有开箱即用且语义清晰的 toString
函数。而在 1.8.20 版本,data class
的语义得到了改进,编译器会自动帮忙生成一些方便的函数:
- toString
- equals
- hashCode
无 copy 和 componentN 函数,这是和
data class
的区别。
基本用法
核心就是提高 object
对象 toString
函数返回值的可读性,特别是针对继承 sealed class
的场景。
开启编译选项
tasks.withType<KotlinCompile> {
kotlinOptions.languageVersion = "1.9"
}
或者
tasks
.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>()
.configureEach {
compilerOptions
.languageVersion
.set(
org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9
)
}
toString
sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult
fun main() {
println(Number(7)) // Number(number=7)
println(EndOfFile) // EndOfFile
}
equals
大多数情况下,data object
对象都是单例,但是在部分极端情况(反射或者序列化等场景)会出现多实例的情况,所以请使用 == 双等号进行结构比较,不要使用三等号的引用比较。
import java.lang.reflect.Constructor
data object MySingleton
fun main() {
val evilTwin = createInstanceViaReflection()
println(MySingleton) // MySingleton
println(evilTwin) // MySingleton
// Even when a library forcefully creates a second instance of MySingleton, its `equals` method returns true:
println(MySingleton == evilTwin) // true
// Do not compare data objects via ===.
println(MySingleton === evilTwin) // false
}
fun createInstanceViaReflection(): MySingleton {
// Kotlin reflection does not permit the instantiation of data objects.
// This creates a new MySingleton instance "by force" (i.e., Java platform reflection)
// Don't do this yourself!
return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor<MySingleton>).newInstance()
}
设计动机
data class
会自动帮助开发者生成语义表达能力很强的 toString
方法。但是 object
无此特性,这会导致在 Kotlin 中实现代数数据类型(ADTs)时造成不一致。
sealed class MyList<T> {
data class Cons<T>(val value: T) : MyList<T>()
object Nil : MyList<Nothing>()
}
fun main() {
println(MyList.Cons(42)) // Cons(value=42)
println(MyList.Nil) // test.MyList$Nil@1d251891
}
针对上述问题,开发者目前只能通过两种方式解决:
- 自行覆写
toString
方法,但是会导致模板代码。
object Nil : MyList<Nothing>() {
override fun toString(): String {
return "Nil"
}
}
- 使用带有占位符默认参数的
data class
,但是代码冗余奇怪且参数无意义。
data class Nil(private val placeholder: String = "")
为了更优雅的解决上述问题,data object
应运而生。
sealed class MyList<T> {
data class Cons<T>(val value: T) : MyList<T>()
data object Nil : MyList<Nothing>()
}
fun main() {
println(MyList.Cons(42)) // Cons(value=42)
println(MyList.Nil) // Nil
}
总结
新增 data object
后,Kotlin 语言中的 object
一族已经来到 4 位:
- 常规 object
object Nil : MyList<Nothing>()
- companion object
companion object {
const val TAG = "LearningAndroidOpenCV"
}
- anonymous object
val helloWorld = object {
val hello = "Hello"
val world = "World"
// object expressions extend Any, so `override` is required on `toString()`
override fun toString() = "$hello $world"
}
- data object
data object Nil : MyList<Nothing>()