1. 上一章:第 14 章
  2. 下一章:第 16 章
>Kotlin 图解指南 • 第 15 章

数据类与解构

章节封面图片

上一章的结尾,我们看到了 Kotlin 中所有对象如何从一个名为 Any 的开放类继承三个函数。这些函数是 equals()hashCode()toString()。在本章中,我们将学习数据类,这是超级强大的类,当你有一个主要只是持有属性的不可变类时,它们特别有用。

为了更好地理解数据类,让我们首先了解上述三个函数,并看看当我们重写它们时需要做什么。

重写 equals()

引用相等性

两位父亲在当地公园相遇,他们的女儿跟在身后,此时他们每个人都不小心松开了女儿的手。

父亲们不小心松开了各自女儿的手,因为女孩们被野生动物吸引了注意力。

这两个女孩都叫 Fiona。然而,尽管她们有相同的名字她们是两个不同的女孩,所以当父亲们转过身来时,重要的是每个人都要找到自己的女儿,而不是他看到的第一个叫 Fiona 的女孩!

女孩们交叉走过,父亲们看着。

类似地,在 Kotlin 中,有时我们想检查一个对象是否是我们想要的实例。为此,我们可以使用相等运算符,即两个相邻的等号 ==。让我们通过创建一个类来表示这两个女孩,并实例化每个对象来演示这一点。

class Child(val name: String)

val fiona1 = Child("Fiona")
val fiona2 = Child("Fiona")

println(fiona1 == fiona2) // false
val fiona1 = (一个名叫 Fiona 的女孩); val fiona2 = (另一个名叫 Fiona 的女孩)

当在这些对象上使用相等运算符时,我们看到它们相等。换句话说,它们是两个不同的 Child 对象。这正是我们想要的!毕竟,这两个女孩是不同的孩子,尽管她们都叫同一个名字。

当然,如果我们把一个 Child 对象赋值给两个不同的变量,那么相等运算符会表明这两个变量是相等的,因为它们都引用了同一个对象实例。

val fiona1 = Child("Fiona")
val fiona2 = fiona1

println(fiona1 == fiona2) // true
val fiona1 = (一个名叫 Fiona 的女孩); val fiona2 = (同一个名叫 Fiona 的女孩)

在上面的代码中,fiona1fiona2 都被赋值为同一个对象,所以它们是相等的。

这种相等性有时被称为引用相等性1因为只要两个变量引用同一个对象实例,它们就会被视为相等。默认情况下,当我们将相等运算符用于比较两个对象时,Kotlin 使用引用相等性。

值相等性

第二天,两位父亲再次在公园相遇。这次,他们每个人同时不小心松开了一张五美元钞票

父亲们再次在公园,双方都不小心掉落了一张五美元钞票。

当他们转过身来时,他们看着地上的钞票,但不确定哪张钞票属于哪个人。

父亲们看着地上的五美元钞票,不确定哪张钞票是谁的。

然而,每张钞票的价值相同——都是五美元。与前一天的情况不同,当时对每位父亲来说,捡起他自己的女儿很重要,但在这种情况下,每个人捡起的是不是他掉落的那张钞票并不重要。只要他们每人捡起一张五美元钞票就可以了,因为两张钞票是相等的。换句话说,有些东西只要具有相同的特征就可以互换。

两个女孩,都叫 Fiona,但她们每个人都是独特的。两张美元钞票,都是五美元,它们是可以互换的。

类似地,在 Kotlin 中,当对象根据其属性值而不是其身份被视为相等时,这通常被称为值相等性2

既然我们认为两张五美元钞票彼此相等,我们可能希望 DollarBill 类具有值相等性而不是引用相等性。以下代码与上面的清单 15.1大致相同,但使用 DollarBill 类而不是 Child 类。

class DollarBill(val amount: Int)

val bill1 = DollarBill(5)
val bill2 = DollarBill(5)

// We want this to be true!
println(bill1 == bill2) // false

正如你所看到的,当我们运行这段代码时,得到的是 false。我们如何才能让这段代码打印出 true

在底层,当我们使用相等运算符时,它调用 equals() 函数来确定两个对象是否被视为相等。从 Any 类传递下来的 equals() 实现将检查两个变量是否引用同一个对象,这就是引用相等性是默认值的原因。因此,如果我们希望相等运算符对特定类表现不同,我们就需要在该类中重写 equals() 函数。

对于这个 DollarBill 类,实现这一点的一种简单方法是手动委托 equals() 调用到 amount 属性。让我们尝试在 DollarBill 类中重写这个函数。请注意,在 Any 类中,这个函数的参数类型是 Any?,返回类型是 Boolean,所以我们在这里的 equals() 函数中也使用相同的类型。3

class DollarBill(val amount: Int) {
    override fun equals(other: Any?) = 
        amount.equals(other.amount)
}
Error

嗯,这不管用。问题是 other 参数的类型是 Any?,这样 Kotlin 中的任何两个对象都可以相互比较,即使它们的类型不匹配。由于 other 可能实际上不是一个 DollarBill 对象,它不一定有一个名为 amount 的属性。

要解决这个问题,我们首先需要检查 other 的类型是否是 DollarBill

  • 如果是,我们就可以使用智能转换来比较 amount 值。
  • 如果不是,我们就返回 false
class DollarBill(val amount: Int) {
    override fun equals(other: Any?) =
        if (other is DollarBill) amount.equals(other.amount) else false
}

做了这个修改后,只要满足以下条件,两个 DollarBill 对象就会彼此相等……

  1. 它们都是 DollarBill 的实例。
  2. 它们都有相同的 amount 属性值。

当我们再次运行代码时,我们会看到 billbill2 现在被视为相等了!

val bill1 = DollarBill(5)
val bill2 = DollarBill(5)

println(bill1 == bill2) // true

因此,通过重写 equals() 函数,我们能够为我们的 DollarBill 类赋予值相等性而不是引用相等性!但是,当我们尝试将此类与某些集合类型一起使用时,会出现另一个问题。这就引出了 hashCode() 函数。

重写 hashCode()

小 Fiona 正在开始收集当代美元钞票。为了完成她的收藏,她需要每种七种面额的一张钞票:1 美元、2 美元、5 美元、10 美元、20 美元、50 美元和 100 美元。

七种不同面额的美元钞票。

你可能还记得第 8 章,多个对象可以存储在称为 Set 的集合类型中,这保证了它的每个元素都是唯一的。换句话说,如果你试图向一个集合添加它已经包含的对象,什么都不会改变。

我们可以用 Set 来跟踪 Fiona 的美元钞票。

val denominations = mutableSetOf<DollarBill>()

Fiona 目前已经收集了三种不同面额的钞票:1 美元、2 美元和 5 美元。我们可以把这些钱加到她的收藏中,然后打印出数量,以确认里面有三种不同的物品。

val denominations = mutableSetOf<DollarBill>()

denominations.add(DollarBill(1))
denominations.add(DollarBill(2))
denominations.add(DollarBill(5))

println(denominations.size) // 3

完美!

有一天,Fiona 发现了一张一美元钞票,但不记得是否已经收集过。当我们尝试将第二张一美元钞票添加到集合中时会发生什么?

val denominations = mutableSetOf<DollarBill>()

denominations.add(DollarBill(1))
denominations.add(DollarBill(2))
denominations.add(DollarBill(5))
denominations.add(DollarBill(1)) // duplicate entry!

println(denominations.size) // 4

哎呀!而不是拒绝重复的,denominations 集合愉快地将其作为第四个元素包含了!为什么会出现这种情况?毕竟,我们在 清单 15.5 中已经重写了 DollarBill 类的 equals() 函数!

Set 本身只是一个接口,当我们调用 mutableSetOf() 时,它返回一个名为 LinkedHashSetSet 实现

LinkedHashSet主要使用 equals() 函数来确定它是否已经包含一个对象。相反,它首先使用 hashCode() 函数,只有当集合中已经存在具有相同哈希码的对象时才会调用 equals()4

这就是为什么我们应该在重写 equals() 的任何时候都重写 hashCode()。在我们的例子中,由于我们已经将 equals() 委托给了 amount 属性,我们也可以对 hashCode() 做同样的事情。

class DollarBill(val amount: Int) {
    override fun equals(other: Any?) =
        if (other is DollarBill) amount.equals(other.amount) else false

    override fun hashCode() = amount.hashCode()
}

简单!做了这个修改后,当我们再次运行 清单 15.9 时,集合将正确地忽略重复的一美元钞票,结果大小为 3 而不是 4。

val denominations = mutableSetOf<DollarBill>()

denominations.add(DollarBill(1))
denominations.add(DollarBill(2))
denominations.add(DollarBill(5))
denominations.add(DollarBill(1)) // duplicate entry!

println(denominations.size) // 3 - Success!

嗯,我们的 DollarBill几乎在做我们想要的一切。在我们了解数据类如何让我们的生活更轻松之前,让我们重写 Any 类的第三个也是最后一个函数 toString()

重写 toString()

虽然我们经常使用 println() 函数,但我们还没有仔细研究过它。我们知道,我们可以向这个函数传递一个参数,它会把参数打印到屏幕上。我们经常向它传递一个字符串,像这样:

println("Hello, Kotlin!")

但是,我们不限于传递字符串;我们可以传递任何对象给这个函数!例如,我们可以将 DollarBill 类的一个实例传递给它。

println(DollarBill(100))

当我们运行这个时,会看到如下输出:

DollarBill@64

当我们将一个对象传递给 println() 函数时,它会调用该对象的 toString() 函数。默认情况下,此函数返回如上所示的字符串。这个字符串有三个部分:

  1. 类的名称
  2. 一个 @ 符号
  3. 对象哈希码的十六进制5
DollarBill@64. DollarBill is the name of the class, and 64 is the hash code of the object, in hexadecimal.DollarBill@64Class Name@ SymbolHash Codein Hex

你可能还记得,DollarBill 类在 清单 15.10 中将 hashCode() 调用委托给了 amount 整数。对于整数,哈希码就是整数本身的值。因此,如果我们为整数变量赋值 100,它的哈希码也是 100。但是,由于 toString() 将该哈希码转换为十六进制,它显示为 64。

这个类中的 toString() 函数如果能显示类的名称和 amount 属性的值(以十进制而不是十六进制表示)会更有帮助。当我们这样做时,最好包含一个标签来指示该数字代表什么(即”amount”)。

让我们重写 toString() 函数并返回一个包含这些内容的字符串。

class DollarBill(val amount: Int) {
    override fun equals(other: Any?) =
        if (other is DollarBill) amount.equals(other.amount) else false

    override fun hashCode() = amount.hashCode()

    override fun toString() = "DollarBill(amount=$amount)"
}

做了这个修改后,当我们用 DollarBill 对象调用 println() 时,会得到更有用的输出。

println(DollarBill(100))
DollarBill(amount=100)

太棒了!

嗯,我们现在对我们的 DollarBill 类做了很多修改。让我们回顾一下我们所做的所有事情。

  1. 我们使得两个 DollarBill 实例彼此相等,只要它们具有相同的 amount 值。换句话说,我们赋予了它值相等性而不是引用相等性
  2. 我们使得它们在集合和映射中也被视为相等。
  3. 我们创建了一个更有用的 toString() 实现。

我们通过重写 Any 类的三个函数——equals()hashCode()toString()——实现了所有这些功能。既然我们理解了为什么可能要重写这些函数,现在终于可以看看数据类如何让我们的生活更轻松了!

数据类简介

在我们做了所有修改之后,这是我们最终的代码:

class DollarBill(val amount: Int) {
    override fun equals(other: Any?) =
        if (other is DollarBill) amount.equals(other.amount) else false

    override fun hashCode() = amount.hashCode()
    override fun toString() = "DollarBill(amount=$amount)"
}

DollarBill 的例子相当简单。毕竟,它只有一个单一属性。如果我们尝试为包含多个属性的类实现相同的功能,会发生什么?实现 toString() 是很直接的。然而,equals() 会更困难一些,而 hashCode()复杂!

好消息是,无论我们的类有一个属性还是十二个属性,Kotlin 都可以自动完成我们在本章中已经做的所有事情——重写 equals()hashCode()toString()——我们只需要声明我们的类是一个数据类。为此,我们只需在类声明前添加 data 关键字,像这样:

data class DollarBill(val amount: Int)

在这段代码中,我们没有为 equals()hashCode()toString() 提供重写。事实上,这个 DollarBill 根本没有类体!然而,仅用一行代码,这个类就做到了 清单 15.16所有的事情

val bill1 = DollarBill(100)
val bill2 = DollarBill(100)

bill1 == bill2                  // true
mutableSetOf(bill1, bill2).size // 1
println(bill1)                  // DollarBill(amount=100)

同样,DollarBill 类只包含一个单一属性,但数据类可以轻松地为具有多个属性的类提供结构相等性和良好的 toString() 结果。例如,这是一个具有三个属性的 Address 类。

data class Address(
    val street: String,
    val city: String,
    val postalCode: String
)

正如接下来的代码清单所演示的,Address 的实例使用值相等性,并且它们打印出来也很好看。

val address1 = Address("123 Maple Ave", "Berrytown", "56789")
val address2 = Address("123 Maple Ave", "Berrytown", "56789")

address1 == address2                  // true
mutableSetOf(address1, address2).size // 1
println(address1)                     // Address(street=123 Maple Ave, city=Berrytown, postalCode=56789)

我们已经看到数据类给了我们很多能力,自动生成有用的 equals()hashCode()toString() 实现。然而,超能力还不止于此!数据类还包括一个名为 copy() 的函数,我们接下来会看到。

复制数据类

数据类的属性可以用 valvar 声明,但 Kotlin 开发者倾向于主要将数据类用于不可变数据。换句话说,在数据类中只使用 val 属性是很常见的。

如你所知,当一个类有一个可变属性时,我们可以简单地为其分配一个新值。例如,这里我们有一个表示一本书的数据类。它的 title 属性是只读的,但其 price 属性是可变的。

data class Book(val title: String, var price: Int)

当书籍价格上涨时,我们可以直接设置它的新值。

val book = Book("The Malt Shop Caper", 18)

// The price just went up!
book.price = 20

price 属性用 var 声明时,更改它的值是足够简单的。但是用 val 声明的属性呢?

当然,我们不能更改 val 属性的值,但我们可以创建整个对象的一个副本,在新副本中替换新值。这种方法类似于我们在第 8 章中使用的方法,当时我们通过使用加号运算符将元素添加到现有列表来创建新列表。

让我们更新 Book 类,使其 price 属性用 val 声明。

data class Book(val title: String, val price: Int)

做了这个修改后,当价格上涨时,我们可以创建一个名为 newBook变量,它与原书具有相同的标题,但具有新价格

val book = Book("The Malt Shop Caper", 18)

// The price just went up!
val newBook = Book(book.title, 20)

现在,用只有两个属性来做这件事是足够简单的,但当我们添加更多属性时,制作副本变得更加乏味。为了演示这一点,让我们向 Book 类再添加四个属性。

data class Book(
    val title: String,
    val price: Int,
    val author: String,
    val width: Int,
    val height: Int,
    val isbn: String,
)

val book = Book("The Malt Shop Caper", 18, "Slim Chancery", 6, 9, "020516918K")

// The price just went up!
val newBook = Book(book.title, 20, book.author, book.height, book.width, book.isbn)

这是很多样板代码,当将值从旧对象传递到新对象时,很容易不小心搞混一些东西。在上面的代码中,你注意到高度和宽度值被交换了吗?

为了避免所有这些样板代码并减少这类错误的可能性,数据类有一个强大的函数叫做 copy()。对于上面的书的例子,我们可以直接调用原始 book 的 copy(),而不是手动将每个属性传递给 Book 构造函数,并给它一个新值来代替 price,像这样。

val newBook = book.copy(price = 20)

copy() 函数为数据类中的每个属性都有一个参数。由于我们的 Book 数据类有 6 个属性,它的 copy() 函数共有 6 个参数,每个参数都默认为属性的当前值。

Code for `Book` and code for a call to copy(), with arrows between them, showing that the parameters and properties correspond to each other in the order that they were declared in the constructor.dataclassBook(valtitle: String,valprice: Int,valauthor: String,valwidth: Int,valheight: Int,valisbn: String)book.copy(title: String = ,price: Int = ,author: String = ,width: Int = ,height: Int = ,isbn: String =)

调用 copy() 函数时,只需为你想要更改的任何属性包含命名参数,而为你想要保持不变的任何属性省略参数。在 清单 15.26 中,我们只提供了 price 参数,所以 newBook 的价格将是 20,但所有其他属性将与原始 book 对象中的值相同。

正如你所看到的,copy() 函数是处理不可变类的一种难以置信的强大方式,它允许我们的代码有效地更改数据,而实际上不改变单个属性!

数据类还提供了另一个功能,即解构其属性的能力,让我们深入了解一下!

解构

当我们有很多都涉及某个概念的值时,通常将它们放在一起是有意义的。例如,如果我们有标题、价格、作者、宽度、高度和 ISBN,我们通常不想将它们作为单独的变量来处理。相反,我们希望将它们分组到一个结构中,比如 Book 类。

Pulling disparate variables together into a single class.val title: Stringval price: Intval author: Stringval width: Intval height: Intval isbn: StringBook+ title: String+ price: Int+ author: String+ width: Int+ height: Int+ isbn: String

这是我们之前使用的Book数据类,它将上面提到的所有变量组合在一起。

data class Book(
    val title: String,
    val price: Int,
    val author: String,
    val width: Int,
    val height: Int,
    val isbn: String,
)

当我们像这样将值组装成一个类时,这些不同的值都彼此关联并且共同代表书籍的概念,这是很清楚的。这通常使开发者更容易理解——例如,title 代表书的标题而不是电影的标题,这很清楚。这也使我们方便地将所有这些属性从一个函数传递到另一个函数——而不是将每个属性作为单独的参数传递,我们可以直接将整个 Book 对象传递。6

所以 again,将关联值放在一起到一个结构中通常是有意义的。

然而,有时将单个值从结构中分离出来也是有意义的,这样它们就存储在单独的变量中。

Pulling the properties back out of a class, into individual variables, which is called 'destructuring'.val title: Stringval price: Intval author: Stringval width: Intval height: Intval isbn: StringBook+ title: String+ price: Int+ author: String+ width: Int+ height: Int+ isbn: String

这被称为解构。当然,我们可以手动完成。例如,在下面的代码中,我们将 book 对象的所有六个属性提取到单独的变量中,其中一些名称与原始属性不同。

val title = book.title
val cost = book.price
val author = book.author
val widthInInches = book.width
val heightInInches = book.height
val isbn = book.isbn

在处理数据类时,我们可以使用解构赋值自动将每个属性提取到变量中,而不必手动提取每个属性。为此,不要声明单个变量名,而是声明多个变量名(用逗号分隔),并将它们全部放在括号中,像这样:

val (title, cost, author, widthInInches, heightInInches, isbn) = book

这与清单 15.28中的代码做的是同样的事情,但全部在一个赋值语句中完成。请注意,这些值将根据属性在数据类主构造函数中出现的顺序进行赋值。对于上面的清单 15.29,这意味着title变量将被赋值为book.title的值,cost变量将被赋值为book.price的值,以此类推。

Arrows between the properties of the Book class and the individual variables of the destructuring assignment. This shows how destructuring is applied in the same order that the properties are declared in the constructor of the class.Destructuring componentsare in the order theproperties aredeclared.dataclassBook(valtitle: String,valprice: Int,valauthor: String,valwidth: Int,valheight: Int,valisbn: String)val(title, cost, author, width, height, isbn) = book

解构赋值中输出的每个值——如 title、price、author 等——称为组件7

最后,请注意在使用解构赋值时我们不必为所有组件赋值。例如,如果只需要从book对象中取出 title 和 price,可以选择在括号中只包含两个变量名。

val (title, cost) = book

解构与标准库

解构不仅适用于我们自己的数据类。Kotlin 标准库中的一些类型也可以被解构。例如,在第 9 章中,我们使用了一个名为Pair的类,它可以与解构赋值一起使用。

val association = "Nail" to "Hammer"
val (hardware, tool) = association

使用解构最常见的方式之一是与lambda 参数一起使用。例如,假设我们有一个包含硬件和工具的映射,而不是单独的关联。

val toolbox = mapOf(
    "Nail" to "Hammer",
    "Bolt" to "Wrench",
    "Screw" to "Screwdriver"
)

当我们遍历这些工具时,lambda 参数有一个名为Map.Entry的类型,它有两个属性——keyvalue。这就是我们在清单 9.20中使用它的方式。

toolbox.forEach { entry ->
    println("Use a ${entry.value} on a ${entry.key}")
}

Map.Entry对象可以被解构,因此我们可以使用括号和两个变量名来替代使用entry作为这个 lambda 的参数,像这样:

toolbox.forEach { (hardware, tool) ->
    println("Use a $tool on a $hardware")
}

当我们这样做时,这个 lambda 的参数将被解构为两个不同的变量——第一个是条目的键,第二个是条目的值。在上面的代码中,键将被赋值给名为 hardware 的变量,值将被赋值给名为 tool 的变量。

这里的解构是个好主意,因为 hardwaretool 是与我们的代码解决的问题相关的术语。我们可以说这些术语处于问题域业务域。将这些术语与诸如条目等术语进行对比,后者更专注于我们用来实现解决方案的数据结构。我们可以说那些术语处于技术域

现在让我们考虑这样一种情况:我们只想打印出工具箱中的工具,而不是相应的硬件。当然,我们可以这样做:

toolbox.forEach { (hardware, tool) ->
    println("Found a $tool")
}

在这里,hardware 变量与 lambda 无关。一个值被赋给它,但我们没有使用它,所以它只是噪音。换句话说,当我们看这段代码时,我们必须读取它,但它不影响代码的工作方式。在这种情况下,当其中一个组件不需要时,我们可以用下划线 _ 代替不相关变量的名称,像这样:

toolbox.forEach { (_, tool) ->
    println("Found a $tool")
}

做了这个更改后,我们不再需要关心一个未使用的 hardware 变量。

解构非数据类

解构并不局限于数据类。任何对象都可以被解构,只要它的类包含正确的函数。秘诀是添加名为 component1()component2()component3() 等的函数。这些被称为 componentN() 函数。例如,这里是本章开头来自 清单 15.1Child 类,但这个类增加了一个孩子年龄的新属性。

class Child(val name: String, val age: Int)

如果我们想给这个类添加解构的能力,不必将其转换为数据类。相反,我们可以简单地添加名为component1()component2()的函数,每个函数返回其中一个属性。

class Child(val name: String, val age: Int) {
    operator fun component1() = name
    operator fun component2() = age
}

有了这个,我们现在可以使用解构来提取孩子的姓名和年龄。

val children = listOf(
    Child("Fiona", 5),
    Child("Jack", 7)
)

children.forEach { (name, age) -> 
    println("$name is $age years old.")
}

请注意,我们必须在 清单 15.38 中的两个函数上包含 operator 修饰符。这是我们第一次创建运算符函数。当一个函数包含 operator 修饰符时,它仍然可以像任何其他函数一样调用,但它也有某种特殊用途——而它所服务的特定目的取决于函数的名称。当一个函数的命名如 清单 15.38 中所示(即像 componentN()),这种特殊用途是该函数将在对象被解构时使用。

我们甚至可以创建一个扩展函数,为一个没有源代码的类添加解构能力。例如,通过一点独创性,我们可以创建自己的扩展函数来将 Double 解构为其整数和小数部分。

operator fun Double.component1() = toString().split(".").first().toInt()
operator fun Double.component2() = toString().split(".").last().toInt()

val (integral, fractional) = 108.245
println(integral)   // 108
println(fractional) // 245

所以,解构在某些情况下是有帮助的,数据类是轻松为我们的类添加解构能力的一种方式!

数据类的局限性

正如我们在本章中所看到的,数据类给了我们很多便利!

  • 它们为值相等性创建 equals()hashCode() 函数。
  • 它们创建美观的 toString() 输出。
  • 它们使创建对象的副本变得容易。
  • 它们可以用于解构赋值。

但是,当我们声明一个类为数据类时,我们也会放弃一些东西,让我们快速看一下这些。

数据类与继承

数据类的第一个也是最重要的缺点是它不能被另一个类扩展。换句话说,你不能将 abstractopen 修饰符添加到数据类。但是,数据类本身可以扩展另一个类。

Two UML class diagrams showing how inheritance cannot and can be used with data classes. Product + id: String data Book + title: String data Product + id: String Book + title: String

尽管数据类可以扩展另一个类,但如果那个超类需要构造函数参数,事情会很快变得复杂,这是因为第二个缺点,它与构造函数参数有关。

构造函数参数

第二个缺点是数据类中的所有构造函数参数必须属性参数。换句话说,每一个都必须用 valvar 声明。这意味着不可能添加一个传递给超类构造函数的构造函数参数。

open class Product(val id: String)

data class Book(id: String, val title: String) : Product(id)
Error

此外,数据类的主构造函数必须至少有一个参数。

最后,虽然数据类可能有不属于其构造函数的属性,但这些属性不会被包含在任何生成的函数中。例如,在下面的代码中,serialNumber 属性不在主构造函数中。

data class DollarBill(val amount: Int) {
    var serialNumber: String? = null
}

serialNumber这样的属性不会在equals()hashCode()toString()中被考虑,因为它是在类的函数体中声明的,而不是在主构造函数中。它不能使用copy()调用,也不会用于解构赋值。为了演示这一点,下面的代码展示了两个DollarBill对象如何被认为是相等的,即使它们具有不同的serialNumber值。

val bill1 = DollarBill(5).apply { serialNumber = "QB12345678T" }
val bill2 = DollarBill(5).apply { serialNumber = "IE87654321C" } 

println(bill1 == bill2) // true, despite different serial numbers

尽管有这些缺点,数据类还是非常有用的,特别是当处理主要是为持有属性而设计的不可变类时。

总结

在本章中,我们学习了关于数据类和解构的全部内容,包括:

在下一章中,我们将介绍另一个类修饰符,它将使我们可以穷举接口或抽象类的每一个可能的子类。下一章见!

感谢James Lorenzen审阅本章!


  1. 这有时也称为恒等相等性。 ↩︎

  2. 你也可能听到过称为内容相等性结构相等性。 ↩︎

  3. 在重写函数时,我们通常使用与超类中同一函数指定的相同参数类型和返回类型。但是,Kotlin 也允许你指定更通用的参数类型或更具体的返回类型。这个特性称为型变,我们将在第 19 章学习泛型时进一步了解型变是如何工作的。 ↩︎

  4. 请注意,同样的问题也适用于映射,因为mutableMapOf()返回的是一个名为HashMap的基于哈希的实现。 ↩︎

  5. 我们大多数人在使用数字时,每个数字有十个可能的值——0 到 9。由于有十个可能的值,这有时被称为十进制或十进制数系。十六进制是一种每个数字最多可以有 16 个值的进位制,所以也称为 Base-16。在 0-9 之后是 A、B、C、D、E 和 F。例如,十六进制中的”C”代表十进制中的 12,而十六进制中的 10 代表十进制的 16。 ↩︎

  6. 虽然这样很方便,但也值得考虑函数实际需要多少个对象值。当你传递的值超过函数所需时,这称为印记耦合,它会限制函数的可重用性。例如,如果函数只需要一个标题,最好只传递标题而不是整个书籍对象,因为这样它就不只能处理书籍标题——也许它还能处理电影和歌曲标题。 ↩︎

  7. ”组件”这个术语含义广泛,可以指软件开发和系统架构学科中的许多东西。这里,我们只是在解构这个非常狭窄的上下文中使用这个术语。 ↩︎