1. 上一章:第 7 章
  2. 下一章:第 9 章
Kotlin 图解指南 • 第 8 章

集合:列表和集合

章节封面图片

到目前为止,我们只把变量当作单独的值来使用。一旦开始以集合(collection)的方式将变量组合起来工作,Kotlin 代码就会变得有趣得多!要学习集合,让我们去拜访 Libby 吧,她是一位聪明伶俐的年轻女士,总是在身边放着一本书!

谁喜欢阅读书籍?

Libby 是一位狂热的读者。她总是在寻找精彩的小说,所以每当有人向她推荐好书时,她就把书名记在一张纸上。以下是目前她列表中的书名:

一张纸,上面列着 Libby 想读的书籍清单。 The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens 待读书籍

Libby 在业余时间一直在学习编写 Kotlin 代码,所以她也想用 Kotlin 来写这个列表,并把所有书名打印到屏幕上。以下是她写的代码:

val book1 = "Tea with Agatha"
val book2 = "Mystery on First Avenue"
val book3 = "The Ravine of Sorrows"
val book4 = "Among the Aliens"
val book5 = "The Kingsford Manor Mystery"

println(book1)
println(book2)
println(book3)
println(book4)
println(book5)

”嗯……”她想。”每次添加一本新书,我都得创建一个新变量。而且保持编号的顺序也很困难——如果我从列表中间移除book3,那么book4book5就需要改名为book3book4。要是有更好的方法来管理我的书名列表就好了……”

列表简介

幸运的是,这里有一个更好得多的方法!Libby 可以创建一个集合(collection)。让我们从 Kotlin 中最常见的集合类型之一开始——列表(list)。创建列表很容易——只需调用listOf(),并将你想要的值作为参数,用逗号分隔即可。让我们更新 Libby 的代码,使用列表。

val booksToRead = listOf(
    "Tea with Agatha",
    "Mystery on First Avenue",
    "The Ravine of Sorrows",
    "Among the Aliens",
    "The Kingsford Manor Mystery",
)

这段代码看起来和 Libby 手写的列表非常相似。事实上,让我们来比较一下两者!

Libby 手写列表与 Kotlin 列表之间的相似性。 The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens 待读书籍 val booksToRead = listOf ( "Tea with Agatha" , "Mystery on First Avenue" , "The Ravine of Sorrows" , "Among the Aliens" , "The Kingsford Manor Mystery" , ) 列表名称 元素 First Last

手写列表和 Kotlin 列表有很多共同点:

  1. 首先,它们都有一个名称。在 Kotlin 中,保存列表的变量名就像是纸上的列表名称。
  2. 其次,两个列表中都有项目——在这个例子中是书名。在 Kotlin 中,列表中的项目称为元素(elements)
  3. 最后,两个列表中的书名都有特定的顺序

以前,我们使用println()函数将变量的内容打印到屏幕。你也可以对集合变量使用println()

println(booksToRead)

当你这样做时,你会按顺序看到它的元素,如下所示:

[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, Among the Aliens, The Kingsford Manor Mystery]

集合与类型

在 Kotlin 中处理集合时,我们需要考虑两种不同的类型

  1. 我们使用的集合的类型。
  2. 集合中元素的类型。

这两个要素共同决定了集合变量的整体类型。以代码清单 8.2为例:

  1. 集合是一个List(列表)。
  2. 集合中元素的类型是String(字符串)。
集合的类型是列表,因为我们使用了 listOf。元素的类型是 String,因为所有值都是字符串。 val booksToRead = listOf ( "Tea with Agatha" , "Mystery on First Avenue" , "The Ravine of Sorrows" , "Among the Aliens" , "The Kingsford Manor Mystery" , ) 我们使用的集合 类型是 List。 这些元素都是 String 类型。

一旦你知道这两点,就能很容易地写出集合变量的类型。先写集合的类型,然后在左尖括号<和右尖括号>之间写元素的类型。因此,booksToRead的类型是List<String>

Kotlin 中字符串列表的类型。List 是集合类型,String 是元素类型。 List<String> 集合的 类型 元素的 类型

让我们重写代码清单 8.2,这次显式地包含booksToRead集合变量的类型信息。

val booksToRead: List<String> = listOf(
    "Tea with Agatha",
    "Mystery on First Avenue",
    "The Ravine of Sorrows",
    "Among the Aliens",
    "The Kingsford Manor Mystery",
)

这种类型是泛型(generic)的一个实例。我们将在后续章节中详细介绍泛型。现在,你只需要知道如何编写列表的类型,以防需要将它用作函数的参数类型或返回值类型。

添加和删除元素

Libby 刚从朋友 Rebecca 那听说了一本很棒的新书!她准备把这本名为《Beyond the Expanse》的新书添加到她的列表中。她该怎么做呢?

当然,她可以直接在listOf()末尾添加一个参数。但如果是在列表已经创建之后添加书名呢?

在 Kotlin 中,一旦你调用listOf()创建了一个列表,该列表就不能再被更改。你不能向其中添加任何内容,也不能从中删除任何内容。在编程中,”更改”的雅称是变更(mutate),因此不允许添加或删除元素的列表称为不可变列表(immutable list)

虽然你不能从常规 Kotlin List中添加或删除元素,但你可以通过将原始列表与新元素组合来创建一个新列表。要做到这一点,请使用加法运算符(plus operator)。即使用+将原始列表与新项目连接起来,并将其赋值给一个新变量,如下所示:

val booksToRead = listOf(
    "Tea with Agatha",
    "Mystery on First Avenue",
    "The Ravine of Sorrows",
    "Among the Aliens",
    "The Kingsford Manor Mystery",
)

val newBooksToRead = booksToRead + "Beyond the Expanse"

在这段代码中,booksToRead + "Beyond the Expanse"是一个表达式,它的计算结果是一个新的List实例。因此,当这段代码运行完毕后,我们有两个集合变量——booksToReadnewBooksToRead

这就像是在第二张纸上写下新的书单。这样,Libby 实际上拥有了两份列表——原始列表和新列表:

两张纸——一张是原始列表,一张是新列表。 The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens 待读书籍 The Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens 新待读书籍

你可能还记得第 1 章中的内容,变量可以用valvar来声明,包括保存集合的变量。但请注意,用var声明集合变量并不会改变列表本身不可变的事实。换句话说,仅仅用var声明它并不能让你添加或删除元素。但是,var确实允许你将另一个不可变列表赋值给它。

因此,通过将booksToRead变量从val改为var,新列表可以赋值给现有的变量名,如下所示:

var booksToRead = listOf(
    "Tea with Agatha",
    "Mystery on First Avenue",
    "The Ravine of Sorrows",
    "Among the Aliens",
    "The Kingsford Manor Mystery",
)

booksToRead = booksToRead + "Beyond the Expanse"

这就像是扔掉旧的纸制列表,然后直接给新列表起一个和旧列表同样的名字。

原始列表被丢弃,新列表取而代之。 The Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens 待读书籍 Tea with Agatha 待读书籍

Libby 的列表现在有六本书了。就在她以为列表更新完毕时,Rebecca 又发来了消息。”你知道吗,我上周读了《Among the Aliens》。真的不太好看,”她说。”你不应该浪费时间读那本。”

Libby 想把那一本从列表中划掉。正如你可能猜到的,你可以用类似的方式从列表中删除元素,但不使用加法运算符,而是使用减法运算符(minus operator)

var booksToRead = listOf(
    "Tea with Agatha",
    "Mystery on First Avenue",
    "The Ravine of Sorrows",
    "Among the Aliens",
    "The Kingsford Manor Mystery",
)

booksToRead = booksToRead + "Beyond the Expanse"
booksToRead = booksToRead - "Among the Aliens"

实际上,最后这两行可以合并成一行,如下所示:

booksToRead = booksToRead + "Beyond the Expanse" - "Among the Aliens"

现在,当 Libby 执行println(booksToRead)时,她在屏幕上看到以下内容:

[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, The Kingsford Manor Mystery, Beyond the Expanse]

”太棒了!”她心想,”我的待读书单全部更新完毕了!”

List 和 MutableList

到目前为止,我们一直在使用常规的 Kotlin List,它不允许被更改,正如我们在上面看到的那样。相反,我们必须使用加法或减法运算符来创建一个新列表。

但是,Kotlin 也提供了另一种列表——一种确实允许你更改它的列表。由于这些列表允许更改,它们被称为可变列表(mutable lists),它们的类型是MutableList

使用可变列表时,你可以使用add()remove()函数来添加或删除元素,如下所示:

val booksToRead: MutableList<String> = mutableListOf(
    "Tea with Agatha",
    "Mystery on First Avenue",
    "The Ravine of Sorrows",
    "Among the Aliens",
    "The Kingsford Manor Mystery",
)

booksToRead.add("Beyond the Expanse")
booksToRead.remove("Among the Aliens")

使用可变列表有点像 Libby 用铅笔而不是钢笔来写她的纸质列表。她可以擦掉一个书名,或者添加一个新的,而不需要用另一张纸。

A handwritten list with one removal and one addition. The Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Among the Aliens Books to Read

Libby 准备好开始阅读那些书了!但为了知道从哪本书开始阅读,她需要知道如何从列表中获取单个书名。

从列表中获取元素

”好吧,我列表上的第一本书是什么?”Libby 思考着。她低头看了看手写的页面。她很容易看出哪本是第一本。”Tea with Agatha,”她记下来了。”现在我如何在 Kotlin 中从列表里获取第一本书呢?”

识别手写列表中的第一项。 The Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Books to Read First Item

如前所述,列表中的元素按照特定顺序排列,这个顺序对于从列表中获取单个元素非常重要。下面是它的工作原理:

列表中的每个元素都会被赋予一个数字,称为索引(index),这个数字基于元素在列表中的位置。第一个元素的索引是0,第二个是1,第三个是2,以此类推。

The first element has index 0, the second has index 1, and so on. 0 1 2 3 4 val booksToRead = listOf ( "Tea with Agatha" , "Mystery on First Avenue" , "The Ravine of Sorrows" , "The Kingsford Manor Mystery" , "Beyond the Expanse" )

一旦知道索引,从列表中获取元素就很容易了。只需调用get()成员函数,将索引作为参数传入。例如,Libby 可以通过如下方式调用get(0)来获取列表中的第一个元素:

val booksToRead = listOf(
    "Tea with Agatha",
    "Mystery on First Avenue",
    "The Ravine of Sorrows",
    "The Kingsford Manor Mystery",
    "Beyond the Expanse"
)

val firstBook = booksToRead.get(0)
println(firstBook) // Tea with Agatha

”太棒了!”Libby 说。”现在我可以轻松地从书单中获取单个书名了!”

除了直接调用get()函数,你还可以使用索引访问运算符(indexed access operator),它用左括号[和右括号]表示,中间是索引。下列代码清单中的代码与上面代码的功能完全相同。

val firstBook = booksToRead[0] 
println(firstBook) // Tea with Agatha

Kotlin 开发者使用索引访问运算符的频率比使用get()函数高得多,所以我们之后将使用索引访问运算符。

现在,从列表中获取单个元素可能很有帮助,但当我们想要对列表中的每个元素执行某种操作时,集合就变得特别有用了。让我们接下来看看如何做到这一点!

循环和迭代

”现在,我想把书单打印到屏幕上,”Libby 自言自语道。”我用println(booksToRead)来实现!” 运行代码后,她看到了以下输出:

[Tea with Agatha, Mystery on First Avenue, The Ravine of Sorrows, The Kingsford Manor Mystery, Beyond the Expanse]

”虽然能这么容易地打印列表很好,但我真的想像手写列表那样竖着看列表。”

她心中设想的是这样的:

Tea with Agatha
Mystery on First Avenue
The Ravine of Sorrows
The Kingsford Manor Mystery
Beyond the Expanse

当然,要实现这一点,她可以逐一地对每个元素调用println(),如下所示:

println(booksToRead[0])
println(booksToRead[1])
println(booksToRead[2])
println(booksToRead[3])
println(booksToRead[4])

但是,像这样编写代码相当乏味。此外,如果按错误顺序打印元素,或者意外多次打印同一元素,很容易出错。实际上,这看起来很像代码清单 8.1中的代码!

不用为列表中的每个元素逐一编写相同的代码,如果 Kotlin 可以遍历每个元素,一个接一个地对其调用 println(),那会怎么样呢?

幸运的是,这在 Kotlin 中非常容易实现!我们可以使用 forEach() 函数。下面是它的用法。

booksToRead.forEach { element -> 
    println(element)
}

当 Kotlin 运行这段代码时,它会先对第一个元素执行 println(element),然后回过头来再对第二个元素执行它,然后再回过头来对第三个元素执行它,以此类推。通过反复执行这行代码,就好像在循环转圈一样,就像这样:

当使用上面的 `forEach()` 时,Kotlin 遍历 `println(element)` 语句,依次将其应用于每个元素。 "The Ravine of Sorrows" "Tea with Agatha" "The Kingsford Manor Mystery" "Beyond the Expanse" "Mystery on First Avenue" println ( element )

这就是为什么编程语言称其为 循环(loop)——因为对于集合中的每个元素,它都会循环回到那段代码。它通常也被称为 迭代(iterating),每次运行代码时,都被称为一次 迭代(iteration)

让我们仔细看看 forEach(),以理解为什么我们要以这种方式构建代码。

forEach() 是存在于集合变量上的成员函数。它是一个接受高阶函数(higher-order function),该函数接受一个lambda 表达式。这个 lambda 表达式就是你希望 Kotlin 对集合中的每个元素运行的代码。

分解 `forEach()` 函数。 Lambda parameter Code to run for each element booksToRead. forEach { element -> println (element) } Collection variable

这里我们将参数命名为 element,但你也可以将其命名为 title。或者,由于这个 lambda 只有一个参数,你可以使用隐式 ‘it’ 参数,这样会更加简洁。事实上,我们可以把它放在一行上:

booksToRead.forEach { println(it) }

无论哪种情况,结果正是 Libby 想要的——书名被垂直打印出来,就像在她的纸质记事本上一样!

Tea with Agatha
Mystery on First Avenue
The Ravine of Sorrows
The Kingsford Manor Mystery
Beyond the Expanse

集合操作

Libby 准备把她感兴趣的书单分享给其他感兴趣的人,从她的朋友 Nolan 开始。但是,当她为他制作列表副本时,她想对一些书名进行修改。

”我真的想去掉每个书名开头的’The’这个词,”Libby 想。”这样我就能按字母顺序排序了,而且以’The’开头的书名就不会挤在一起了。”

映射集合:转换元素

有时当你从现有集合创建一个新集合时,你也希望以某种方式转换其中一个或多个元素。在 Libby 的例子中,她想删除书名开头的”The”,以便它们可以用于排序。

创建一个新列表,其中以"The"开头的书名不再包含该词。 The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows Beyond the Expanse 待读书籍 Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue Ravine of Sorrows Sortable Titles

在对所有书名进行这种转换之前,让我们先从其中一个开始。String 对象有一个 removePrefix() 函数,你可以使用它来删除字符串开头的单词。以下是它的用法:

val sortableTitle = "The Kingsford Manor Mystery".removePrefix("The ")

println(sortableTitle) // Kingsford Manor Mystery

完美!现在她只需要将这个 removePrefix() 函数应用到列表中的每个元素上即可!

”也许我可以使用 forEach(),因为我知道它对列表中的每个元素进行操作”,Libby 想。她撸起袖子,写出了以下代码:

val sortableTitles: MutableList = mutableListOf()
 
booksToRead.forEach { title ->
    sortableTitles.add(title.removePrefix("The "))
}

sortableTitles.forEach { println(it) }

”嗯,这个方法可行,”Libby 想。”但有点复杂,而且要写的代码很多……”

这之所以复杂,是因为 Libby 想要创建一个新集合,但 forEach() 做不到这一点。它只是对现有集合运行 lambda,然后返回 Unit。她真正需要的是一个能运行 lambda 并将 lambda 的结果作为元素包含在新集合中的集合操作。

在 Kotlin 中,这个集合操作叫做 map()。下面是 Libby 如何使用它来从新集合中书名的开头移除”The”这个词:

val sortableTitles = booksToRead.map { title -> 
    title.removePrefix("The ") 
}

这段代码的作用与前一个代码清单相同(只是结果是一个不可变的 List 而不是 MutableList)。与 forEach() 一样,map() 函数对列表中的每个元素调用一次 lambda。然而, forEach() 不同的是map() 会在每次迭代中使用 lambda 的结果来构建一个新列表。

当使用上面的 `map()` 时,Kotlin 遍历 `title.removePrefix("The ")` 表达式,依次将其应用于每个元素。 "The Ravine of Sorrows" "Tea with Agatha" "The Kingsford Manor Mystery" "Beyond the Expanse" "Mystery on First Avenue" title . removePref ix ( "The " ) "Ravine of Sorrows" "Tea with Agatha" "Kingsford Manor Mystery" "Beyond the Expanse" "Mystery on First Avenue"

当你打印列表中的每个元素时,你可以看到 The Ravine of SorrowsThe Kingsford Manor Mystery 都已被更新,使得”The”这个词不再位于开头。

Tea with Agatha
Mystery on First Avenue
Ravine of Sorrows
Kingsford Manor Mystery
Beyond the Expanse

让我们仔细看看 map() 函数:

Breakdown of the `map()` function. Lambda parameter Evaluates to a value that becomes an element in the new list. val sortableTitles = booksToRead. map { title -> title. removePrefix ( "The " ) } Original list variable New list variable
  • forEach() 类似,map() 函数是一个接受 lambda 的高阶函数
  • 该 lambda 将对列表中的每个元素运行一次。
  • lambda 的结果将成为新集合中的一个元素。
  • map() 函数返回该新集合。

forEach()map()这样的函数被称为集合操作(collection operations),因为它们是对集合执行某种操作的函数。

”完美!”Libby 说,”既然书名已经按我想要的方式更改了,也许我可以给它们排序?”

排序集合

forEach()map()函数只是 Kotlin 中众多集合操作中的两个。另一个非常有用的函数叫做sorted()

由于 map() 函数返回一个集合,Libby 可以在调用 map() 之后直接调用 sorted(),如下所示:

val sortedTitles = booksToRead.map { title -> title.removePrefix("The ") }.sorted()

当她打印出 sortedTitles 的元素时,她看到了她希望看到的输出!

Beyond the Expanse
Kingsford Manor Mystery
Mystery on First Avenue
Ravine of Sorrows
Tea with Agatha

为了让代码更易于阅读,每个集合操作可以单独成行,如下所示:

val sortedTitles = booksToRead
    .map { title -> title.removePrefix("The ") }
    .sorted()

此代码与前一个代码清单相同,只是格式不同。换句话说,所有的字母和标点符号完全相同且顺序一致——只是它们之间的空格不同。

像这样垂直书写集合操作会很有帮助,因为它可以让你很容易地向下扫视各行,看看涉及哪些集合操作以及它们的顺序。例如,首先对书名进行映射,然后对书名进行排序。因此,Kotlin 开发者经常这样格式化他们的代码。

过滤集合:包含和省略元素

Libby 非常兴奋!现在她有一份按字母顺序排列的书单,可以分享给 Nolan 了。

”我迫不及待想看到你的书单了,”Nolan 说。”记住——我只读推理小说!”

”只要推理小说……?”Libby 重复道。”好吧,”她心里想,”我最后一件事就是要删除不是推理小说的书名。”她从笔记本上撕下一页,只为 Nolan 写了一份定制列表,省略了所有不是推理小说的书名。

创建一个新列表,只包含原始列表中的某些书名。 Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue Ravine of Sorrows Beyond the Expanse 待读书籍 Kingsford Manor Mystery Mystery on First Avenue Books for Nolan

”我如何在 Kotlin 中做到这一点?”她想知道。

你可能已经猜到了,Kotlin 包含一个使这变得容易的集合操作,而且毫不意外,它叫做 filter()

就像空气过滤器阻止灰尘和过敏原进入你的空调系统一样,Kotlin 列表过滤器会阻止你不希望进入新列表的元素!

让我们使用 filter() 函数来筛选列表中的书籍,只保留标题中包含”Mystery”的书籍:

val booksForNolan = booksToRead
    .map { title -> title.removePrefix("The ") }
    .sorted()
    .filter { title -> title.contains("Mystery") }

filter() 函数与上面的 map() 函数类似——它接受一个 lambda 作为参数,并且该 lambda 将对原始列表中的每个标题调用一次。然而,与 map() 函数不同的是,filter() 的 lambda 必须返回一个 Boolean。如果它对某个元素返回 true,则该元素会被传入新集合(即本例中的 booksForNolan)。如果它返回 false,则该元素会被省略,不出现在新集合中。

When using `filter()` above, Kotlin loops over the `title.contains("Mystery")` expression, giving it each element in turn. "The Ravine of Sorrows" "Tea with Agatha" "The Kingsford Manor Mystery" "Beyond the Expanse" "Mystery on First Avenue" title . contains ( "Mystery" ) false false true false true

下面是 filter() 函数用法的详细分解:

Breakdown of the `filter()` function. Lambda parameter Determines whether this element is included in the new list. val booksForNolan = booksToRead. filter { title -> title. contains ( "Mystery" ) } Original list variable New list variable

打印列表中的每个元素,Libby 看到的结果如下:

Kingsford Manor Mystery
Mystery on First Avenue

”很好,”她说。”列表正是我想要的样子。它只包含推理小说,而且排序正确!”

集合操作链

让我们再看一遍那段代码:

val booksForNolan = booksToRead
    .map { title -> title.removePrefix("The ") }
    .sorted()
    .filter { title -> title.contains("Mystery") }

在 Kotlin 中,像这样将多个集合操作一个接一个地放在一起是很常见的。当我们这样做时,这被称为链式调用(chaining)集合操作——每个操作就像是链条中的一环。在这个代码清单中,map()sorted()filter() 调用被链接在一起。

A collection operation chain. Each operation is like one link in the chain. val booksForNolan = booksToRead . map { title -> title. removePrefix ( "The " ) } . sorted () . filter { title -> title. contains ( "Mystery" ) } Collection operation chain

请记住,操作链不是在更改单个列表。事实上,这些操作中的每一个都会创建一个新列表最终操作 filter() 创建的列表被赋值给变量 booksForNolan中间列表——即链内部的集合操作创建的列表——被链中的下一个操作使用,但没有赋值给任何变量。不过,记住这些中间列表仍然很重要。下面的插图显示了链中每一步涉及的列表。

A new list is created at each step of the operation chain. Kingsford Manor Mystery Mystery on First Avenue Beyond the Expanse Kingsford Manor Mystery Mystery on First Avenue Ravine of Sorrows Tea with Agatha Tea with Agatha Mystery on First Avenue Ravine of Sorrows Kingsford Manor Mystery Beyond the Expanse Tea with Agatha Mystery on First Avenue The Ravine of Sorrows The Kingsford Manor Mystery Beyond the Expanse val booksForNolan = booksToRead . map { title -> title. removePrefix ( "The " ) } . sorted () . filter { title -> title. contains ( "Mystery" ) }

当你遇到这样的集合操作链时,考虑每个中间列表中有多少元素会很有帮助。例如,代码清单 8.21 中的代码将 filter() 调用放在链的末尾。但如果它放在链的开头会怎样呢?就像这样:

val booksForNolan = booksToRead
    .filter { title -> title.contains("Mystery") }
    .map { title -> title.removePrefix("The ") }
    .sorted()

通过这样做,filter() 产生的中间列表只有两个元素,在这种情况下,map() 函数只需要调用其 lambda 两次而不是五次,sorted() 也只需要排序两个元素而不是五个。在这个例子中,无论哪种方式,最终列表都是相同的,但 代码清单 8.22 可能比 代码清单 8.21 更高效。

下面的插图显示了将 filter() 调用放在顶部时每一步涉及的列表。请注意,中间列表的元素比前一个插图中的要少。

Fewer elements are processed in `map()` and `sorted()` when `filter()` runs first. Kingsford Manor Mystery Mystery on First Avenue Mystery on First Avenue Kingsford Manor Mystery Mystery on First Avenue The Kingsford Manor Mystery Tea with Agatha Mystery on First Avenue The Ravine of Sorrows The Kingsford Manor Mystery Beyond the Expanse val booksForNolan = booksToRead . filter { title -> title. contains ( "Mystery" ) } . map { title -> title. removePrefix ( "The " ) } . sorted ()

在这样的小列表上,这没什么大不了的,但在一个有数百或数千个元素的列表上,你可以看到这如何提高代码的性能——也就是说,它运行得更快!

其他集合操作

Kotlin 还有许多其他易于使用的集合操作!下面列出几个可能会对你有帮助的,让你先了解一下。

  • drop(3) - 新列表省略原始列表中的前 3 个元素。
  • take(5) - 新列表只使用原始列表中的前 5 个元素。
  • distinct() - 新列表将省略重复的元素,使每个元素只包含一次。
  • reversed() - 新列表将包含与原始列表相同的元素,但顺序相反。

你可以在 Kotlin API 文档中看到更完整的列表

集合简介

在我们结束本章之前,值得注意的是,列表并不是 Kotlin 中唯一的一种集合。列表可能是最常用的,但另一种有用的集合类型叫做集合(Set)。列表有助于确保其元素按特定顺序排列,而集合有助于确保其中的每个元素始终是唯一的

例如,Nolan 最喜欢的推理小说作家 Slim Chancery 写了三本书,Nolan 骄傲地说他收集了完整的套装

在 Kotlin 中创建集合和创建列表一样容易。只需使用setOf()mutableSetOf(),而不是listOf()mutableListOf()

val booksBySlim: Set = setOf(
    "The Malt Shop Caper",
    "Who is Mrs. W?",
    "At Midnight or Later",
)

当你向一个已经包含该值的集合添加元素时,集合将保持不变。

val booksBySlim: MutableSet = mutableSetOf(
    "The Malt Shop Caper",
    "Who is Mrs. W?",
    "At Midnight or Later",
)

booksBySlim.add("The Malt Shop Caper")

println(booksBySlim)
// [The Malt Shop Caper, Who is Mrs. W?, At Midnight or Later]

请注意,当打印集合或对其使用集合操作时,集合不能保证其元素的顺序。元素的顺序可能与你添加它们的顺序相同,但不要依赖这一点!

因为集合的元素没有特定顺序,所以它们的元素没有索引。出于这个原因,集合甚至不包含 get() 函数!

关键要点是:

  1. 列表中的元素有保证的顺序,并且可以包含重复元素。
  2. 集合中的元素没有特定顺序,并且保证不包含重复元素。

另外,你可以将列表转换为集合,反之亦然。只需使用 toSet()toList()。请记住,如果你将列表转换为集合,你将丢失重复元素,而且顺序可能会不同!

val bookList = listOf(
    "The Malt Shop Caper",
    "At Midnight or Later",
    "The Malt Shop Caper",
)

val bookSet = bookList.toSet()         // bookSet has two elements
val anotherBookList = bookSet.toList() // anotherBookList also has two elements

总结

在本章之前,我们只处理单独的变量。通过使用列表和集合这样的集合,我们能够对整组值进行操作,这开辟了全新的可能性世界!在本章中,你学习了集合,包括:

我们发现通过索引从列表中获取元素非常容易。然而,有时你需要一种通过其他信息获取元素的简单方法。例如,你可能想通过 ISBN(背面条形码上方的那个长数字)来获取一本书,而不是通过位置索引。在下一章中,我们将学习另一种对元素进行分组的方法,使你能轻松做到这一点!届时见!