Scala 3 — 书籍

集合方法

语言

Scala 集合的一个巨大优势是,它们开箱即用,包含几十种方法,并且这些方法始终可以在不可变和可变集合类型中使用。这样做的好处是,您不再需要每次使用集合时都编写自定义 for 循环,并且当您从一个项目迁移到另一个项目时,您会发现使用这些相同的方法,而不是更多自定义 for 循环。

几十种方法可供您使用,因此此处并未全部显示。相反,仅显示一些最常用的方法,包括

  • map
  • filter
  • foreach
  • head
  • tail
  • taketakeWhile
  • dropdropWhile
  • reduce

以下方法适用于所有序列类型,包括 ListVectorArrayBuffer 等,但这些示例使用 List,除非另有说明。

作为一个非常重要的注意事项,List 上的任何方法都不会改变列表。它们都以函数式风格工作,这意味着它们会返回一个包含修改结果的新集合。

常用方法示例

为了让你概览一下在以下部分中将看到的内容,这些示例展示了一些最常用的集合方法。首先,这里有一些不使用 lambda 的方法

val a = List(10, 20, 30, 40, 10)      // List(10, 20, 30, 40, 10)

a.distinct                            // List(10, 20, 30, 40)
a.drop(2)                             // List(30, 40, 10)
a.dropRight(2)                        // List(10, 20, 30)
a.head                                // 10
a.headOption                          // Some(10)
a.init                                // List(10, 20, 30, 40)
a.intersect(List(19,20,21))           // List(20)
a.last                                // 10
a.lastOption                          // Some(10)
a.slice(2,4)                          // List(30, 40)
a.tail                                // List(20, 30, 40, 10)
a.take(3)                             // List(10, 20, 30)
a.takeRight(2)                        // List(40, 10)

高阶函数和 lambda

接下来,我们将展示一些常用的接受 lambda(匿名函数)的高阶函数 (HOF)。首先,这里有几种 lambda 语法的变体,从最长的形式开始,逐步过渡到最简洁的形式

// these functions are all equivalent and return
// the same data: List(10, 20, 10)

a.filter((i: Int) => i < 25)   // 1. most explicit form
a.filter((i) => i < 25)        // 2. `Int` is not required
a.filter(i => i < 25)          // 3. the parens are not required
a.filter(_ < 25)               // 4. `i` is not required

在那些编号的示例中

  1. 第一个示例展示了最长的形式。这种冗长性很少需要,并且仅在最复杂的使用情况下需要。
  2. 编译器知道 a 包含 Int,因此这里不需要重新声明这一点。
  3. 当你只有一个参数(例如 i)时,不需要括号。
  4. 当你只有一个参数,并且它只在匿名函数中出现一次时,你可以用 _ 替换该参数。

匿名函数 提供了更多详细信息和示例,说明了与缩短 lambda 表达式相关的规则。

现在你已经看到了简洁的形式,这里有一些使用短形式 lambda 语法的其他 HOF 示例

a.dropWhile(_ < 25)   // List(30, 40, 10)
a.filter(_ > 100)     // List()
a.filterNot(_ < 25)   // List(30, 40)
a.find(_ > 20)        // Some(30)
a.takeWhile(_ < 30)   // List(10, 20)

需要注意的是,HOF 也接受方法和函数作为参数,而不仅仅是 lambda 表达式。这里有一些 map HOF 的示例,它使用名为 double 的方法。再次展示了 lambda 语法的几种变体

def double(i: Int) = i * 2

// these all return `List(20, 40, 60, 80, 20)`
a.map(i => double(i))
a.map(double(_))
a.map(double)

在最后一个示例中,当匿名函数包含一个采用单个参数的函数调用时,你不需要命名该参数,因此甚至不需要 _

最后,你可以根据需要组合 HOF 来解决问题

// yields `List(100, 200)`
a.filter(_ < 40)
 .takeWhile(_ < 30)
 .map(_ * 10)

示例数据

以下部分中的示例使用这些列表

val oneToTen = (1 to 10).toList
val names = List("adam", "brandy", "chris", "david")

map

map 方法逐个遍历现有列表中的每个元素,将你提供的函数逐个应用于每个元素;然后它返回一个包含所有修改元素的新列表。

以下是将 map 方法应用于 oneToTen 列表的示例

scala> val doubles = oneToTen.map(_ * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

你还可以使用长形式编写匿名函数,如下所示

scala> val doubles = oneToTen.map(i => i * 2)
doubles: List[Int] = List(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

但是,在本课中,我们始终使用第一个较短的形式。

以下是将 map 方法应用于 oneToTennames 列表的几个示例

scala> val capNames = names.map(_.capitalize)
capNames: List[String] = List(Adam, Brandy, Chris, David)

scala> val nameLengthsMap = names.map(s => (s, s.length)).toMap
nameLengthsMap: Map[String, Int] = Map(adam -> 4, brandy -> 6, chris -> 5, david -> 5)

scala> val isLessThanFive = oneToTen.map(_ < 5)
isLessThanFive: List[Boolean] = List(true, true, true, true, false, false, false, false, false, false)

如最后两个示例所示,使用 map 返回与原始类型不同的类型的集合是完全合法的(也是常见的)。

filter

filter 方法创建一个包含满足所提供谓词的元素的新列表。谓词或条件是一个返回 Booleantruefalse)的函数。以下是一些示例

scala> val lessThanFive = oneToTen.filter(_ < 5)
lessThanFive: List[Int] = List(1, 2, 3, 4)

scala> val evens = oneToTen.filter(_ % 2 == 0)
evens: List[Int] = List(2, 4, 6, 8, 10)

scala> val shortNames = names.filter(_.length <= 4)
shortNames: List[String] = List(adam)

集合上的函数式方法的一大优点是,你可以将它们链接在一起以解决问题。例如,此示例展示了如何链接 filtermap

oneToTen.filter(_ < 4).map(_ * 10)

REPL 显示结果

scala> oneToTen.filter(_ < 4).map(_ * 10)
val res1: List[Int] = List(10, 20, 30)

foreach

foreach 方法用于循环处理集合中的所有元素。请注意,foreach 用于副作用,例如打印信息。以下是有 names 列表的示例

scala> names.foreach(println)
adam
brandy
chris
david

head 方法来自 Lisp 和其他早期的函数式编程语言。它用于访问列表的第一个元素(头元素)

oneToTen.head   // 1
names.head      // adam

由于 String 可以看作是一系列字符,因此你也可以将其视为列表。这是 head 在这些字符串上工作的方式

"foo".head   // 'f'
"bar".head   // 'b'

head 是一个很棒的方法,但需要注意的是,当在空集合上调用它时,它也会抛出异常

val emptyList = List[Int]()   // emptyList: List[Int] = List()
emptyList.head                // java.util.NoSuchElementException: head of empty list

因此,你可能希望使用 headOption 而不是 head,尤其是在以函数式风格编程时

emptyList.headOption          // None

如所示,它不会抛出异常,它只是返回类型 Option,其值为 None。你可以在 函数式编程 章节中了解有关此编程风格的更多信息。

tail

tail 方法也来自 Lisp,它用于打印列表中头元素之后的每个元素。几个示例演示了这一点

oneToTen.head   // 1
oneToTen.tail   // List(2, 3, 4, 5, 6, 7, 8, 9, 10)

names.head      // adam
names.tail      // List(brandy, chris, david)

就像 head 一样,tail 也适用于字符串

"foo".tail   // "oo"
"bar".tail   // "ar"

如果列表为空,tail 会抛出 java.lang.UnsupportedOperationException,因此,就像 headheadOption 一样,还有一个 tailOption 方法,在函数式编程中更受欢迎。

列表也可以匹配,因此你可以编写类似这样的表达式

val x :: xs = names

将该代码放入 REPL 中显示 x 被分配到列表的头部,而 xs 被分配到尾部

scala> val x :: xs = names
val x: String = adam
val xs: List[String] = List(brandy, chris, david)

这种模式匹配在许多情况下很有用,例如使用递归编写 sum 方法

def sum(list: List[Int]): Int = list match {
  case Nil => 0
  case x :: xs => x + sum(xs)
}
def sum(list: List[Int]): Int = list match
  case Nil => 0
  case x :: xs => x + sum(xs)

taketakeRighttakeWhile

taketakeRighttakeWhile 方法为你提供了一种很好的方式来“获取”你想要用来创建新列表的列表中的元素。这是 taketakeRight

oneToTen.take(1)        // List(1)
oneToTen.take(2)        // List(1, 2)

oneToTen.takeRight(1)   // List(10)
oneToTen.takeRight(2)   // List(9, 10)

注意这些方法如何处理“边缘”情况,即我们要求的元素多于序列中的元素,或要求零个元素

oneToTen.take(Int.MaxValue)        // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.takeRight(Int.MaxValue)   // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.take(0)                   // List()
oneToTen.takeRight(0)              // List()

这是 takeWhile,它使用谓词函数

oneToTen.takeWhile(_ < 5)       // List(1, 2, 3, 4)
names.takeWhile(_.length < 5)   // List(adam)

dropdropRightdropWhile

dropdropRightdropWhile 本质上与其“take”对应项相反,从列表中删除元素。以下是一些示例

oneToTen.drop(1)        // List(2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.drop(5)        // List(6, 7, 8, 9, 10)

oneToTen.dropRight(8)   // List(1, 2)
oneToTen.dropRight(7)   // List(1, 2, 3)

再次注意这些方法如何处理边缘情况

oneToTen.drop(Int.MaxValue)        // List()
oneToTen.dropRight(Int.MaxValue)   // List()
oneToTen.drop(0)                   // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
oneToTen.dropRight(0)              // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

这是 dropWhile,它使用谓词函数

oneToTen.dropWhile(_ < 5)       // List(5, 6, 7, 8, 9, 10)
names.dropWhile(_ != "chris")   // List(chris, david)

reduce

当你听到术语“map reduce”时,“reduce”部分指的是 reduce 等方法。它采用一个函数(或匿名函数)并将该函数应用于列表中的连续元素。

解释 reduce 的最佳方法是创建一个你可以传递给它的助手方法。例如,这是一个 add 方法,它将两个整数相加,还为我们提供了一些不错的调试输出

def add(x: Int, y: Int): Int = {
  val theSum = x + y
  println(s"received $x and $y, their sum is $theSum")
  theSum
}
def add(x: Int, y: Int): Int =
  val theSum = x + y
  println(s"received $x and $y, their sum is $theSum")
  theSum

给定该方法和此列表

val a = List(1,2,3,4)

当你将 add 方法传递到 reduce 中时,就会发生这种情况

scala> a.reduce(add)
received 1 and 2, their sum is 3
received 3 and 3, their sum is 6
received 6 and 4, their sum is 10
res0: Int = 10

正如该结果所示,reduce 使用 add 将列表 a 缩减为单个值,在本例中,即列表中整数的总和。

一旦习惯了 reduce,就可以像这样编写“求和”算法

scala> a.reduce(_ + _)
res0: Int = 10

类似地,“乘积”算法如下所示

scala> a.reduce(_ * _)
res1: Int = 24

关于 reduce 需要了解的一个重要概念是,顾名思义,它用于将集合缩减为单个值。

更多

Scala 集合类型中有几十种其他方法,可以让你永远不需要再编写另一个 for 循环。请参阅 可变和不可变集合Scala 集合的架构,以获取有关 Scala 集合的更多详细信息。

最后一点,如果你在 Scala 项目中使用 Java 代码,则可以将 Java 集合转换为 Scala 集合。通过这样做,你可以在 for 表达式中使用这些集合,还可以利用 Scala 的函数式集合方法。有关更多详细信息,请参阅 与 Java 交互 部分。

此页面的贡献者