第37条:使用数据修饰符来表示一组数据
有时我们只需要传递一组数据,这就是我们使用 data class 的目的。这些是带有 data
修饰符的类,从我的经验来看,开发人员很快就会把它引入到他们的数据模型类中:
data class Player(
val id: Int,
val name: String,
val points: Int
)
val player = Player(0, "Gecko", 9999)
当我们添加 data
修饰符时,它会生成一些有用的函数:
toString
equals
和hashCode
copy
componentN
(component1
、component2
等)
让我们根据数据类型依次讨论它们。
toString
显示类名和所有主构造函数属性的值及其名称。有助于日志的展示和调试:
print(player) // Player(id=0, name=Gecko, points=9999)

equls
检查是否所有的主构造函数属性都相等,并且 hashCode
与它是一致的(请看_第41条:遵守hashCode的合约_)。
player == Player(0, "Gecko", 9999) // true
player == Player(0, "Ross", 9999) // false
copy
对于不可变数据类型非常有用,它创建了一个新的对象,其中每个主构造函数属性在默认情况下都具有相同的值,但可以使用具名参数更改它们:
val newObj = player.copy(name = "Thor")
print(newObj) // Player(id=0, name=Thor, points=9999)
我们无法看到 copy
方法的实现,因为它是在底层生成的,就像通过 data
修饰符生成的其它方法一样,如果我们能看到它,大概就是下面生成 Person
的样子:
// This is how `copy` is generated under the hood by
// data modifier for `Person` class looks like
fun copy(
id: Int = this.id,
name: String = this.name,
points: Int = this.points
) = Player(id, name, points)
注意,copy
方法对一个对象做了浅拷贝,但是当对象不可变的时候,这并不是问题 —— 对于不可变的对象,不需要深拷贝。
componentN
函数(component1
、component2
等等)允许基于其位置的解构,就像下面这个例子:
val (id, name, pts) = player
Kotlin 中的解构直接转化为使用 componentN
函数中的变量来定义,因此上面的代码将被编译成下面这段代码:
// After compilation
val id: Int = player.component1()
val name: String = player.component2()
val pts: Int = player.component3()
基于位置的解构有优点和缺点。最大的优点是我们可以随意命名变量,我们也可以分解我们想要的一切,只要它提供 componentN
函数。 List
和 Map.Entry
都能体现它 :
val visited = listOf("China", "Russia", "India")
val (first, second, third) = visited
println("$first $second $third") // China Russia India
val trip = mapOf(
"China" to "Tianjin",
"Russia" to "Petersburg",
"India" to "Rishikesh"
)
for ((country, city) in trip) {
println("We loved $city in $country")
// We loved Tianjin in China
// We loved Petersburg in Russia
// We loved Rishikesh in India
}
另一方面,它是危险的,当类中的元素顺序发生变化时,我们需要调整每一个解构。它也很容易因混乱的顺序而错误的分解:
data class FullName(
val firstName: String,
val secondName: String,
val lastName: String
)
val elon = FullName("Elon", "Reeve", "Musk")
val (name, surname) = elon
print("It is $name $surname!") // It is Elon Reeve!
我们需要小心地解构。使用和主构造函数中相同的属性的名称是很有用的,然后,不正确的顺序,IntelliJ / Android Studio 将会显示警告。甚至可以将警告升级为错误。

不要像下面的例子那样分解得到第一个值:
data class User(val name: String)
val (name) = User("John")
这可能会让读者感到困惑,特别是当你在 lambda 表达式中进行分解时:
data class User(val name: String)
fun main() {
val user = User("John")
user.let { a -> print(a) } // User(name=John)
// 不要这样做
user.let { (a) -> print(a) } // John
}
这是有问题的,因为在一些语言中,lambda 表达式中包住参数的括号是可选或必需的。
优先使用 data class 替代元组(tunples)
data class 提供的功能比元组更多。更具体的说,Kotlin 元组只是可序列化的通用数据类型,并且有一个自定义的 toSring
方法:
public data class Pair<out A, out B>(
public val first: A,
public val second: B
) : Serializable {
public override fun toString(): String ="($first, $second)"
}
public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C
) : Serializable {
public override fun toString(): String = "($first, $second, $third)"
}
为什么我只展示了 Pair
和 Triple
? 这是因为它们是 Kotlin 最后剩下的元组类型。Kotlin 在 beta 版本就有无限元组了。我们可以通过括号和一组类型来定义类型,如: (Int, String, String, Long)。最后,我们所实现的行为与数据类相同,但可读性差很多。你能猜出这组类型代表什么吗? 它可以是任何东西,使用元组很诱人,但使用 data class 几乎总是更好!这就是为什么元组被删除,只留下 Pair
和 Triple
,它们被保留下是出于一些在小范围内使用的意图,例如:
当我们需要立即命名值时:
val (description, color) = when {
degrees < 5 -> "cold" to Color.BLUE
degrees < 23 -> "mild" to Color.YELLOW
else -> "hot" to Color.RED
}
表示一个事先没有被定义的组合 —— 通常在标准库中使用:
val (odd, even) = numbers.partition { it % 2 == 1 }
val map = mapOf(1 to "San Francisco", 2 to "Amsterdam")
在其它情况下,我们更喜欢 data class。让我们来看一个例子,假设我们需要为一函数来将 fullname 解析为 name 和 surnname,可以将这个 name 和 surnname 表示为 Pair<String, String>
:
fun String.parseName(): Pair<String, String>? {
val indexOfLastSpace = this.trim().lastIndexOf(' ')
if(indexOfLastSpace < 0) return null
val firstName = this.take(indexOfLastSpace)
val lastName = this.drop(indexOfLastSpace)
return Pair(firstName, lastName)
}
// Usage
val fullName = "Marcin Moskała"
val (firstName, lastName) = fullName.parseName() ?: return
问题是,当有人阅读它时,他并不清楚 Pair<String, String>
里面的每个类型分别表示的是什么。更重要的是,这些值的顺序并不清楚,有人认为可能 surnname 会在前面:
val fullName = "Marcin Moskała"
val (lastName, firstName) = fullName.parseName() ?: return
print("His name is $firstName") // His name is Moskała
为了使引用更加安全、函数更加易读,我们应该使用数据类型:
data class FullName(
val firstName: String,
val lastName: String
)
fun String.parseName(): FullName? {
val indexOfLastSpace = this.trim().lastIndexOf(' ')
if(indexOfLastSpace < 0) return null
val firstName = this.take(indexOfLastSpace)
val lastName = this.drop(indexOfLastSpace)
return FullName(firstName, lastName)
}
// Usage
val fullName = "Marcin Moskała"
val (firstName, lastName) = fullName.parseName() ?: return
它的成本几乎为0,并且显著改善了功能:
函数的返回类型是明确的
返回类型更短,更容易传递
如果用户解构的变量名不同,则会显示一个警告
如果不希望这个类在更大范围内使用时,可以限制其可见性,如果你只需要在单个文件或类中用某些本地函数处理使用它,它甚至可以是私有的。我们更值得去使用 data class 而不是元组。 Kotlin 的类成本很低,请不要害怕使用它们。
Last updated