Skip to content

Latest commit

 

History

History
2138 lines (1476 loc) · 92.1 KB

13.集合:常见序列方法.md

File metadata and controls

2138 lines (1476 loc) · 92.1 KB

13 集合:常见序列方法

前面两章主要关注的是序列类,而本章的重点是常用的序列方法。但在深入研究这些方法之前,使用集合类方法时有几个重要的概念需要了解:

  • 谓词
  • 匿名函数
  • 隐式循环

谓词

predicate是一个方法、函数或匿名函数。可接收一个或多个输入参数并返回Boolean值。 下面方法返回值是 truefalse,所以是一个谓词:

    def isEven(i: Int): Boolean =
        i % 2 == 0

谓词是一个简单的概念,使用集合方法时,会经常听到这个术语,所以有必要提及。

匿名函数

匿名函数同样是一个重要的概念。在10.1小节“使用函数字面量(匿名函数)”中有深入的描述,作为一个简单的例子,下面代码展示了完整的匿名函数,与 isEven 方法做同样的工作:

    (i: Int) => i % 2 == 0

简写如下:

    _ % 2 == 0

看起来没什么,但当它与集合上的filter方法结合在一起时,一小段代码就会发挥很大的作用:

    scala> val list = List.range(1, 10)
    list: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

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

隐式循环

filter方法是第三个话题的一个很好的例子:implied loops。正如你从上面例子中看到的,filter包含一个循环,将你的函数使用在集合中的每个元素,并返回一个新的集合。你可以不使用filter 方法,而写出下面等价代码:

    for
        e <- list
        if e % 2 == 0
    yield
        e

我觉得你也会认同使用filter方法会使代码更简洁,也更容易阅读。

filterforeachmapreduceLeft等集合方法都在其算法中内置了循环操作。由于循环内置,在编写Scala代码时,你会比其他语言少写很多自定义的for循环。

本章中的小节

虽然序列类有一百多个内置的方法,但本章的小节主要集中在最常用的方法上,包括:

  • filter通过谓词来过滤集合。
  • map对集合中的每个元素应用一个转换函数。
  • 提取序列和现有序列子集的方法。
  • 寻找序列中唯一元素的方法。
  • 合并和压缩序列的方法。
  • 随机化和排序序列的方法。
  • 将序列转换为字符串的两个方法。

这些功能(以及更多的功能)将在下面的小节中展示。

13.1 选择集合方法解决问题

问题

Scala集合里有大量的方法可用,你需要选择其中一个来解决问题。

解决方案

Scala集合类提供了丰富的用来操作数据的方法。绝大多数方法以函数或者谓词作为参数。

可用的方法在本节中会以两种方式列出。接下来的几段中,方法按类别划分,从而使你方便找到所需要的。然后在后面的表格中,还会有简单的描述和方法签名。

按类别划分的方法

过滤方法

可以用来过滤一个集合的方法,包括collectdiffdistinctdropdropRightdropWhilefilterfilterNotfilterInPlacefindfoldLeftfoldRightheadheadOptioninitintersectlastlastOptionslicetailtaketakeRighttakeWhileunion

转换方法

转化器方法至少需要一个输入集合来创建一个新的输出集合,通常使用你提供的算法。包括 ++++:++:appendedappendedAlldiffdistinctcollectconcatflatMapflatteninitsmapmapInPlacepatchreversesortedsortBysortWithsortInPlacesortInPlaceWithsortInPlaceBytailstakeWhileupdatedzipzipWithIndex

分组方法

这些方法会根据一个已有的集合创建多个分组。包括groupBygroupedgroupMappartitionslidingspansplitAtunzip

信息和数学方法

这些方法提供关于集合的信息,包括canEqualcontainscontainsSlicecountendsWithexistsfindfindLastforAllindexOfindexOfSliceindexWhereisDefinedAtisEmptylastlastOptionlastIndexOflastIndexOfSlicelastIndexWherelengthlengthIsmaxmaxBymaxOptionmaxByOptionminminByminOptionminByOptionnonEmptyproductsegmentLengthsizesizeIsstartsWithsum。像foldLeftfoldRightreduceLeftreduceRight这样的方法也可以通过提供一个函数去获得集合的信息。

其他

一些其他的方法很难分类,包括viewforeachaddStringmkStringview在集合上创建一个惰性视图(见11.4小节,“在集合上创建惰性视图”),foreach就像for循环,遍历集合里的每一个元素,并对每个元素产生副作用。addStringmkString会根据集合生成字符串。

甚至还有比这里列出的更多方法。例如,有一系列的 to* 方法,把当前集合(例如List)转换为其他集合类型(toArraytoBuffertoVector等等)。关于集合类的更多的内置方法可以通过Scaladoc查找。

通用集合方法

下表列出了最常见的集合方法。注意,带引号的描述来自每个类的Scaladoc。

表13-1通过Iterable列出了所有集合共有的方法。在这个表中,第一列符号的含义如下:

  • c代表一个集合。
  • f代表一个函数。
  • p代表一个谓词。
  • n代表一个数字。

更多的可变和不可变集合的方法分别在表13-2和表13-3中。

表13-1 Iterable集合的常用方法(scala.collection.Iterable)

方法 描述
c collect f 通过将偏函数应用于已定义函数的集合中所有元素来构建一个新集合。
c count p 对集合中满足谓词的元素的计数。
c drop n 返回集合中除前n个元素外的所有元素。
c dropWhile p 返回一个包含“满足谓词的最长前缀的元素”的集合。
c exists p 如果集合中任何元素的谓词为真,则返回true
c filter p 返回集合中谓词为true的所有元素。
c filterNot p 返回集合中谓词为false的所有元素。
c find p 返回第一个匹配谓词的元素Option[A]
c flatMap f 通过将函数应用于集合c的所有元素(如map),然后将结果集合的元素扁平化,返回一个新的集合。
c flatten 将集合的集合(如列表的列表)转换为单个集合(单个列表)。
c foldLeft(s)(f) 将操作f应用于连续的元素,从左到右(左关联)。从种子值s开始。
c foldRight(s)(f) 将操作f应用于连续的元素,从右到左(右关联),从种子值s开始。
c forAll p 如果所有元素的谓词为真,则返回true,否则返回false
c foreach f 将函数f应用于集合的所有元素(其中f通常是一个副作用函数)。
c groupBy f 根据函数把集合归类成一个Map
c head 返回集合的第一个元素。集合为空则抛出NoSuchElementException
c headOption 返回集合的第一个元素。如果元素存在返回Some[A],否则None
c init 从集合中选择除最后一个以外的所有元素。如果集合是空的,则抛UnsupportedOperationException
c inits ”遍历这个可迭代集合的初始值。“
c isEmpty 如果集合是空的,返回true,否则返回false
c knownSize 集合中元素的数量,如果可以低代价计算,则为 -1。低代价通常意味着:不需要遍历集合。”
c last 返回集合中的最后一个元素。如果集合是空的,会抛出NoSuchElementException
c lastOption 如果元素存在,返回集合的最后一个元素为Some[A],如果集合为空,则返回None
c1 lazyZip c2 一个惰性版本的zip方法。
c map f 通过对集合中的所有元素应用该函数,创建一个新的集合。
c max 返回集合中最大的元素。可以抛出java.lang.UnsupportedOperationException
c maxOption Option返回集合中最大的元素。
c maxBy f 返回函数f所度量的最大元素。可以抛出java.lang.UnsupportedOperationException
c maxByOption Option返回函数f所度量的最大元素。
c min 返回集合中最小的元素。可以抛出java.lang.UnsupportedOperationException
c minOption Option返回集合中最小的元素。
c minBy 返回函数f所度量的最小元素。可以抛出java.lang.UnsupportedOperationException
c minByOption Option返回函数f所度量的最小元素。
c mkString 将序列转换为字符串的几个选项。
c nonEmpty 如果集合至少包含一个元素,则返回true,否则返回false
c partition p 根据谓词算法,返回两个集合。
c product 返回集合中所有元素的乘积。
c reduceLeft op foldLeft相同,但从集合的第一个元素开始。可以抛出 java.lang.UnsupportedOperationException
c reduceRight op foldRight相同,但从集合的最后一个元素开始。可以抛出 java.lang.UnsupportedOperationException
c scanLeft op reduceLeft类似,但返回一个Iterable
c scanRight op reduceRight类似,但返回一个Iterable
c size 返回集合的大小。
c1 sizeCompare(c2) 比较c1c2的大小。 如果c1较小,返回 <0;如果它们大小相同,返回0;如果c1较大,返回 >0
c sizeIs n 将一个集合的大小与整数n进行比较,同时尽可能少地遍历元素。
c slice(from, to) 返回从元素from开始到元素to结束的元素区间。
c sliding(size,step) 通过在序列上传递一个滑动窗口,返回长度为size的序列。step参数允许跳过元素。
c span p 返回两个集合的集合;第一个集合由 c.takeWhile(p) 创建,第二个集合由 c.dropWhile(p) 创建。
c splitAt n 通过在元素n处拆分集合c,返回两个集合的集合。
c sum 返回集合中所有元素的和。
c tail 返回集合中除第一个元素以外的所有元素。
c tails 遍历序列的尾部。
c take n 返回集合的前n个元素。
c takeWhile p 当谓词为true时从集合中返回元素。当谓词为false时停止。
c tapEach f 将一个副作用函数f应用于c中的每个元素,同时也返回c
c unzip zip相反,通过将每个元素分成两部分,将一个集合分解为两个集合,就像分解Tuple2元素集合一样。
c view 返回一个非严格的(惰性)集合视图。
c1 zip c2 通过匹配c1的元素0c2的元素0c1的元素1c2的元素1,等等,创建一个配对集合。
c zipWithIndex 通过索引对集合进行压缩。

还有其他方法,但这些是最常见的。关于更多的方法,参考你正在使用集合的Scaladoc。

可变集合方法

表13-2展示了可变集合的常用方法(这些都是方法,但是某些方法看起来像内置的操作符)。

表13-2可变集合中常用的操作符(方法)

方法 描述
c += x 将元素x添加到集合c中,别名addOne
c1 ++= c2 将集合c2中的元素添加到集合c1中。别名addAll
c −= x 从集合c中删除元素x,别名subtractOne
c −= (x,y,z) 从集合c中删除元素xyz
c1 −−= c2 将集合c2中的元素从集合c1中删除。别名subtractAll
c(n) = x 将值x赋值给元素c(n)
c append x 将元素x追加到集合c中。
c1 appendAll c2 c2中的元素追加到c1集合中。
c clear 删除集合中的所有元素。
c filterInPlace p 保留集合中谓词为true的所有元素。
c flatMapInPlace f 假设c是一个列表的列表,通过对元素应用函数f来更新所有元素。工作方式类似于先map,然后flatten
c mapInPlace f 通过对元素应用该函数来更新集合中的所有元素。
c1.patchInPlace(i,c2,n) 从索引i开始,在序列c2patch,替换元素的数量n。将n设为0,在索引i处插入新的序列。
c prepend x 将元素x前加到集合c中。
c1 prependAll c2 c2中的元素前加到集合c1中。
c sortInPlace 根据Ordering就地排序集合。
c sortInPlaceBy f 根据隐式Ordering与转换函数f对集合就地排序。
c sortInPlaceWith f 根据比较函数f对集合就地排序。
c remove i 删除索引i处的元素。
c.remove(i, len) 删除从索引i开始,长度为len的元素。
c.update(i,e) 将索引i处的元素更新为新值e

注意,像 +=-= 这样的符号方法名是方法的别名。例如 +=addOne 的别名。关于更多的方法,参考你正在使用可变集合的Scaladoc。

不可变集合方法

表13-3展示了不可变集合的常用方法。注意不能修改不可变集合,所以第一列中每个表达式的结果必须被赋值给一个新的变量。(参考11.3小节“理解可变变量与不可变集合”,了解在不可变的集合中使用可变的变量的细节)。

表13-3不可变的集合中特有的方法

方法 描述
c1 ++ c2 通过将集合c2中的元素追加到集合c1中,从而创建一个新的集合。别名concat
c :+ e 返回一个新集合,元素e被追加到集合c
c1 :++ c2 返回一个新的集合,将c2中的元素追加到c1中的元素上。别名appendedAll
e +: c 返回一个新的集合,并将元素e前加到集合c中,别名prepended
c1 ++: c2 返回一个新的集合,其中c1中的元素前加到c2中的元素上。别名prependedAll
e :: list 返回一个带有元素eList,该元素被前加到名为listList上。(:: 只对List有效。)
list1 ::: list2 返回一个List,其中list1中的元素前加到list2中的元素上。( ::: 仅对List有效。)
c updated(i,e) 返回c的拷贝,索引i处的元素被e替换。

注意,像 ++++= 这样的符号方法名是方法的别名。例如,++concat 的别名。要注意的是,对于大多数序列来说,方法 --- 在几个版本之前就被废弃了,目前只在集合上可用。所以要使用表13-1中列出的过滤方法来返回一个新的集合,并删除所需的元素。

上表只列出了不可变集合上最常见的方法。还有其他的方法,比如 --- 方法在不可变集合上是可用的。关于更多的方法,参考你正在使用集合的Scaladoc。

Maps

Map还有附加的方法,如表13-14所示。在这个表中,第一列符号的含义如下:

  • mm1m2代表map
  • mm代表可变map
  • kk1k2代表map的键。
  • p代表一个谓词(返回truefalse的函数)。
  • vv1v2代表map的值。
  • c代表集合。

表13-4可变和不可变map中常用的方法

Map方法 描述
不可变Map的方法
m + (k->v) 返回添加的键/值对的map。也可以用来更新带有键k的键/值对。别名updated
m1 ++ m2 返回map m1m2的组合。也可用于更新键/值对。别名concat
m ++ Seq(k1->v1, k2->v2) 返回map mSeq中元素的组合。也可用于更新键/值对。别名concat
m - k 返回删除键k(以及对应值)的map。别名remove
m - Seq(k1, k2, k3) 返回删除键k1k2k3map。别名removed
m -- k
m -- Seq(k1,k2)
返回删除键的映射。虽然展示是Seq,但可以是任何的IterableOnce别名。removedAll
可变Map的方法
mm(k) = v 将值v分配给键k
mm += (k -> v) 将键/值对添加到可变map mm中。别名addOne
mm ++= Map(k1 -> v1, k2 -> v2) 将多个键/值对添加到可变map mm中。别名addAll
mm ++= List(k1 -> v1, k2 -> v2) 将集合c中的元素添加到可变map mm中。别名addAll
mm -= k 根据给定的键从可变map mm中删除。别名subtractOne
mm --= Seq(k1, k2, k3) 根据给定的多个键从可变map mm中删除。别名subtractAll
可变和不可变Map的方法
m(k) 返回k关联的值。
m contains k 如果map m包含键k则返回true
m filter p 返回键和值都符合谓词p条件的map
m get k 如果找到键,返回键k的值为Some[A],否则为None
m getOrElse(k, d) 如果找到键,则返回键k的值,否则返回默认值d
m isDefinedAt k 如果map包含键k则返回true
m keys Iterable的形式返回map的键。
m keyIterator Iterator的形式返回map的键。
m keySet Set的形式返回map的键。
m values Iterable形式返回map的值。
m valuesIterator Iterator形式返回map的值。

你也可以用 updatedWithupdateWith 方法来更新Map的值,这两个方法分别适用于不可变和可变的Map。

更多相关的方法,请参考可变map类的Scaladoc( https://oreil.ly/OwG1n )和不可变map类的Scaladoc( https://oreil.ly/X5LhZ )。

讨论

正如你所看到的,Scala集合类包含了大量的方法(以及看起来是操作符的方法)。了解这些方法可以帮助你提高工作效率,随着理解的提高,你可以通过简短的函数和谓词来使用这些方法,编写更精炼的代码和更少的循环。

13.2 用foreach遍历一个集合

问题

你想用foreach方法迭代集合中的元素。

解决方案

foreach方法提供一个函数,匿名函数或方法,以匹配foreach正在寻找的方法签名,同时也解决你的问题。

Scala序列上的foreach方法有这样的签名:

    def foreach[U](f: (A) => U): Unit

这意味着它需要一个函数作为方法的唯一的参数,并且该函数需要一个泛型A,并且不返回任何东西(Unit)。比如在实际应用中,A作为集合中的类型,可以是IntString

foreach的工作方式是,从集合中每次传入一个元素给函数,从第一个元素开始,到最后一个元素结束。你提供的函数可以对每个元素做任何希望它做的事情,尽管你的函数不能返回任何东西。(如果你想返回什么,请看map方法)。

举个例子,foreach的常见用法是输出信息,如Vector[Int]

    val nums = Vector(1, 2, 3)

你可以编写一个函数,接受Int参数而没有返回:

    def printAnInt(i: Int): Unit = println(i)

因为printAnIntforeach要求的签名相匹配,你可以将它与numsforeach一起使用:

    scala> nums.foreach(i => printAnInt(i))
    1
    2
    3

你也可以这样写这个表达式:

    nums.foreach(printAnInt(_))
    nums.foreach(printAnInt) // most common

最后一个例子展示了最常用的形式。

同样地,你也可以通过编写一个匿名函数传入foreach来解决这个问题。这些例子都与使用printAnInt函数相同:

    nums.foreach(i => println(i))
    nums.foreach(println(_))
    nums.foreach(println) // most common

Map上使用foreach

foreachMap类上也可使用。foreachMap实现有这样的签名:

    def foreach[U](f: ((K, V)) => U): Unit

这意味着它期望接收有两个参数(KV,代表keyvalue)的函数,同时并返回U,代表Unit。因此,foreach向函数传入两个参数。你可以把这些参数当作一个元组来处理:

    val m = Map("first_name" -> "Nick", "last_name" -> "Miller")

    m.foreach(t => println(s"${t._1} -> ${t._2}")) // tuple syntax

你也可以使用这种方式:

    m.foreach {
        (fname, lname) => println(s"$fname -> $lname")
    }

参阅14.9小节,“遍历Map”,了解遍历Map的其他方法。

副作用 -- 鸽子栏

       如上所示,foreach将函数作用于集合中的每个元素,但函数不需要返回值,foreach也不返回值。因为foreach 不返回任何东西。所以在逻辑上使用它必须有其他的原因,比如打印输出或修改其他变量。因此,有人说tuple-2(二元组)foreach,以及任何其他返回Unit的方法必须用于其副作用。因此,foreach是一个statement ,而不是一个expression。关于语句和表达式的更多细节,参考我的博客“A Note About Expression-Oriented Programming” ( https://oreil.ly/UyODg ) 。

讨论

foreach使用多行函数,需要将函数用花括号包成为代码块,然后传入:

    val longWords = StringBuilder()

    "Hello world it’s Al".split(" ").foreach { e =>
        if e.length > 4 then longWords.append(s" $e")
        else println("Not added: " + e)
    }

REPL中运行该代码,输出如下:

    Not added: it’s
    Not added: Al
    val longWords: StringBuilder = Hello world

另见

  • 你可以使用for循环和for表达式来迭代集合中的元素,详见第4章。

13.3 使用迭代器

问题

你想要(或需要)在Scala程序中使用迭代器。

解决方案

在Scala中使用迭代器,有些要点需要了解:

  • 与Java的while循环不同,Scala开发者一般不会直接使用IteratorhasNextnext方法。
  • 出于性能方面的考虑,使用迭代器是有意义的,比如读取大文件时。
  • 迭代器在使用后会被耗尽。
  • 虽然迭代器不是集合,但它有常用的集合方法。
  • 迭代器的转化器方法是惰性的。
  • Iterator的子类BufferedIterator提供了headheadOption方法,可以查看下一个元素的值。

这些要点(和解决方案)将在下面小节中涵盖。

Scala开发者不会直接使用hasNext和next

尽管在Java中使用迭代器的 hasNext()next() 是遍历集合的常见方式,但Scala开发者通常不会直接使用这些方法。相反,会使用mapfilterforeach 等集合方法遍历集合,或者说for循环。说白了,只要我在Scala中有迭代器,我就从来没有直接写过这样的代码:

    val it = Iterator(1, 2, 3)

    // we don’t do this
    val it = collection.iterator
    while (it.hasNext) ...

相反,我的代码会是这样写的:

    val a = it.map(_ * 2)           // a: Iterator[Int] = <iterator>
    val b = it.filter(_ > 2)         // b: Iterator[Int] = <iterator>
    val c = for e <- it yield e*2   // c: Iterator[Int] = <iterator>

性能原因导致迭代器有意义

虽然没有直接调用 hasNext()next() 方法,但迭代器在Scala中是一个重要概念。例如,当你用io.Source.fromFile 方法读取一个文件时,它返回一个迭代器,使得一次从文件中读取一行。这是有意义的,因为将大的数据文件一次读入内存是不现实的。

迭代器也被用于views的转化器方法中,它是惰性的。例如Programming in Scala 一书展示了用迭代器来实现lazyMap函数:

    def lazyMap[T, U](coll: Iterable[T], f: T => U) =
        new Iterable[U] {
            def iterator = coll.iterator map f
        }

正如11.4小节,“在集合上创建惰性视图”中所示,在大集合上使用视图是提高性能的重要技巧。

迭代器在使用后会被耗尽

使用迭代器的一个重要部分,需要知道迭代器在使用之后会耗尽(变为空)。当访问每个元素时,会改变迭代器(参考讨论),并且前面的元素会被丢弃。例如,如果你使用foreach来打印迭代器的元素,调用在第一次时就起作用了:

    scala> val it = Iterator(1,2,3)
    it: Iterator[Int] = nonempty iterator

    scala> it.foreach(print)
    123

当第二次尝试同样的调用时,将没有任何输出,因为迭代器已经耗尽了:

    scala> it.foreach(print)
    (no output here)

迭代器的行为像集合

从技术上讲,迭代器不是集合。相反,它只提供了可以逐个访问集合中元素的方法。但在迭代器又确实定义了许多在常用集合类中看到的方法,如foreachmapfilter等。你也可以在需要时将迭代器转换为集合:

    val i = Iterator(1,2,3) // i: Iterator[Int] = <iterator>
    val a = i.toVector      // a: Vector[Int] = Vector(1, 2, 3)

    val i = Iterator(1,2,3) // i: Iterator[Int] = <iterator>
    val b = i.toList        // b: List[Int] = List(1, 2, 3)

迭代器是惰性的

另一个重要的点是,迭代器是惰性的,意味着它们的转换器方法以一种非严格或惰性的方式进行计算。例如,下面的for循环,mapfilter方法并不返回具体的结果,而只返回一个迭代器:

    val i = Iterator(1,2,3)         // i: Iterator[Int] = <iterator>

    val a = for e <- i yield e*2    // a: Iterator[Int] = <iterator>
    val b = i.map(_ * 2)            // b: Iterator[Int] = <iterator>
    val c = i.filter(_ > 2)         // c: Iterator[Int] = <iterator>

像其他惰性方法一样,它们仅仅在需要时才会被计算,例如调用严格方法foreach

    scala> i.map(_ + 10).foreach(println)
    11
    12
    13

BufferedIterator允许你提前查看元素

缓冲迭代器也是一种迭代器,它允许你在不向前移动迭代器的情况下查看下一个元素。可以通过调用Iteratorbuffered方法创建BufferedIterator

    val it = Iterator(1,2)  // it: Iterator[Int] = <iterator>
    val bi = it.buffered    // bi: BufferedIterator[Int] = <iterator>

随后在BufferedIterator上调用head方法,这不会影响迭代器:

    // call 'head' as many times as desired
    bi.head // 1
    bi.head // 1
    bi.head // 1

另一方面,注意到在IteratorBufferedIterator上调用next方法时会发生什么:

    // 'next' advances the iterator
    bi.next // 1
    bi.next // 2
    bi.next // java.util.NoSuchElementException: next on empty iterator

小心调用head方法 -- 耗子栏

       正如13.1小节中所讨论的,你通常想用 headOption 而不是 head,因为如果在空列表上调用它,或者在列表的末尾调用它, head 方法会抛出异常。

    // create a one-element BufferedIterator
    val bi = Iterator(1).buffered
        // result: BufferedIterator[Int] = <iterator>

    // 'head' works fine
    bi.head            // 1

    // advance the iterator
    bi.next         // 1
    bi.headOption    // None (headOption works as intended)

    // 'head' blows up
    bi.head
        // result: java.util.NoSuchElementException:
        // next on empty iterator

讨论

从概念上讲,迭代器就像指针。当你在列表上创建迭代器时,它最初指向列表的第一个元素:

    val x = 1 :: 2 :: Nil
            ^

然后当你调用迭代器的next方法时,它会指向集合中的下一个元素:

    val x = 1 :: 2 :: Nil
                 ^

最后,当迭代器到达集合的末尾时,它被认为已经耗尽。不会再回到指向第一个元素的位置:

    val x = 1 :: 2 :: Nil
                       ^

如解决方案中所示,此时调用nexthead将抛出异常java.util.NoSuchElementException

另见

13.4 使用zipWithIndex或者zip创建循环计数器

问题

你想用 for 循环或 foreach 方法循环一个序列,并且想在循环中访问计数器,而不必手动创建一个计数器。

解决方案

zipWithIndex或者zip方法创建一个计数器,假设你有一个字符列表:

    val chars = List('a', 'b', 'c')

使用计数器打印列表中的元素,一种方法是使用zipWithIndexforeach和花括号中的case语句:

    chars.zipWithIndex.foreach {
        case (c, i) => println(s"character '$c' has index $i")
    }

    // output:
    character 'a' has index 0
    character 'b' has index 1
    character 'c' has index 2

正如你将在讨论中所见,这个解决方案之所以有效,因为zipWithIndex返回一系列由tuple-2(二元组)组成的序列,如下:

    List((a,0), (b,1), ...

也是因为代码块中的case语句匹配了一个tuple-2(二元组)。foreach将tuple-2传递给你的算法,你也可以这么写:

    chars.zipWithIndex.foreach { t =>
        println(s"character '${t._1}' has index ${t._2}")
    }

最后,也可以使用for循环:

    for
        (c, i) <- chars.zipWithIndex
    do
        println(s"character '$c' has index $i")

所有的循环都有相同的输出。

用zip控制起始值

当使用zipWithIndex时,计数器总是从0开始。如果你想控制起始值,使用zip

    for (c, i) <- chars.zip(LazyList from 1) do
        println(s"${c} is #${i}")

循环输入如下:

    a is #1
    b is #2
    c is #3

讨论

当在序列中使用zipWithIndex时,它返回一个tuple-2(二元组)元素的序列。例如,给定一个List[Char],你可以看到zipWithIndex产生的是很多tuple-2(二元组)值:

    scala> val chars = List('a', 'b', 'c')
    val chars: List[Char] = List(a, b, c)

    scala> val zwi = chars.zipWithIndex
    val zwi: List[(Char, Int)] = List((a,0), (b,1), (c,2))

在花括号中使用case语句

如解决方案中所示,你可以在花括号内使用case语句和foreach

    chars.zipWithIndex.foreach {
        case (c, i) => println(s"character '$c' has index $i")
    }

这种方法可以用在任何需要使用函数字面量的地方。在其他情况下,你可以根据需要使用尽可能多的case选项。

在Scala 2.13之前,这个例子只能用case关键字来写,但在Scala 2.13及以上版本中,这行代码可以用下面任何一种方式来写:

    case(c, i) => println(s"character '$c' has index $i") // shown previously
    case(c -> i) => println(s"character '$c' has index $i") // alternate
    (c, i) => println(s"character '$c' has index $i") // without the 'case'

使用惰性视图

因为zipWithIndex从一个已有的序列中创建了一个新的序列,你可能会想在调用zipWithIndex之前调用view方法,尤其是大的序列:

    scala> val zwi2 = chars.view.zipWithIndex
    zwi2: scala.collection.View[(Char, Int)] = View(<not computed>)

正如11.4小节,“在集合上创建惰性视图”中所讨论的,这将在char上创建惰性视图,这意味着:

  • 没有创建中间的序列。
  • 需要时元组元素才会被创建,尽管在循环的情况下,通常是需要的,除非你的算法包含终止或异常。

因为使用view可以避免创建中间的集合,在zipWithIndex之前调用view有助于在大集合上循环。通常当性能是一个问题时,使用或不使用视图测试你的代码。

13.5 用map实现集合的转换

问题

像之前的小节一样,你想通过对原始集合中的每个元素应用一种算法,以将一个集合转化为另一个集合。

解决方案

与其像4.4小节“用for/yield从现有集合创建一个新的集合”中展示的那样使用for/yield组合,不如在你的集合上调用map方法,传参一个函数、一个匿名函数或者一个方法来转换每个元素。这些例子展示了如何使用匿名函数:

    val a = Vector(1,2,3)

    // add 1 to each element
    val b = a.map(_ + 1)        // b: Vector(2, 3, 4)
    val b = a.map(e => e + 1)   // b: Vector(2, 3, 4)

    // double each element
    val b = a.map(_ * 2)        // b: Vector(2, 4, 6)
    val b = a.map(e => e * 2)   // b: Vector(2, 4, 6)

下面例子展示了使用函数(或方法):

    def plusOne(i: Int) = i + 1
    val a = Vector(1,2,3)

    // three ways to use plusOne with map
    val b = a.map(plusOne)          // b: Vector(2, 3, 4)
    val b = a.map(plusOne(_))       // b: Vector(2, 3, 4)
    val b = a.map(e => plusOne(e))  // b: Vector(2, 3, 4)

编写一个使用map的方法

当编写使用map的方法时:

  • 方法的参数是和集合类型相同的单个输入参数。
  • 方法的返回类型可以是你需要的任何类型。

例如,假设有一个可以被转换为整数的字符串列表:

    val strings = List("1", "2", "hi mom", "4", "yo")

你可以用map将字符串列表转换为整数列表。首先需要一个方法,(a)接收一个String,(b)返回一个Int。例如,作为示例方法的第一步,如果你想确定列表中每个字符串的长度,lengthOf方法就可以了:

    def lengthOf(s: String): Int = s.length
    val x = strings.map(lengthOf) // x: List(1, 1, 6, 1, 2)

然而,因为我真的想把每个字符串转换为整数,但因为有些字符串不能转换为整数,我真正需要的是一个返回 Option[Int] 的函数:

    import scala.util.Try
    def makeInt(s: String): Option[Int] = Try(Integer.parseInt(s)).toOption

当给定字符串 "1" 时,方法返回 Some(1),而当给定字符串 "yo" 时,返回None

现在可以使用makeIntmap将列表中的每个字符串转换为整数。第一次尝试返回一个 Option[Int]List,即 List[Option[Int]] 类型。

    scala> val intOptions = strings.map(makeInt)
    val intOptions: List[Option[Int]] = List(Some(1), Some(2), None, Some(4), None)

一旦你知道了集合可用的方法,你可以将 List[Option[Int]] 扁平化为List[Int]

    scala> val ints = strings.map(makeInt).flatten
    val ints: List[Int] = List(1, 2, 4)

讨论

当我第一次接触Scala时,我的背景是Java,所以我最初写了for/yield循环。这是我所熟悉的命令式的解决方案。但最终我意识到,map就像没有任何保护的for/yield表达式一样,只有一个生成器。

    val list = List("a", "b", "c")                  // list: List(a, b, c)

    // map
    val caps1 = list.map(_.capitalize)              // caps1: List(A, B, C)

    // for/yield
    val caps2 = for e <- list yield e.capitalize    // caps2: List(A, B, C)

当明白这一点后,我就开始使用map

这是关于Scala集合类中许多函数方法的一个关键概念:像mapfiltertake等方法都是自定义for循环的替代方法。使用这些内置函数方法有很多好处,但两个重要的好处是:

  • 你不需要写自定义的for循环。
  • 你不需要阅读其他开发者编写的自定义for循环。

我并不是在吐槽,刚好相反,我的意思是说,for循环需要大量的冗余代码,而你必须阅读这些代码才能找到其它开发者的意图。当你使用Scala集合内置的方法时,逻辑会更清晰明了。

有一个小例子,给定下面的列表:

    val fruits = List("banana", "peach", "lime", "pear", "cherry")

满足(a)找到所有长度超过两个字符和(b)长度少于六个字符的字符串,然后(c)将剩下的这些字符串大写,命令式的解决方案看起来像这样:

    val newFruits = for
        f <- fruits
        if f.length < 6
        if f.startsWith("p")
    yield f.capitalize

由于Scala的语法,这并不难读,但至少要注意两件事:

  • 你必须明确地写出 f <- fruits,即“for each fruit in fruits”。
  • 算法的一部分在for表达式内,另一部分在yield关键字之后。

相比较一下,对于同样的问题,Scala的常用解决方案是这样的:

    val newFruits = fruits.filter(_.length > 2)
                          .filter(_.startsWith("p"))
                          .map(_.capitalize)

即使在这样的一个小例子中,可以看到编写(a)是你想要的,而不是(b)一步步的命令式算法来得到你想要的东西。一旦了解了如何使用Scala集合方法,你会发现可以更多地关注意图,而不是编写自定义for循环的细节,代码将变得简洁,但是可读性仍然很好 —— 这就是我们所说的 富有表现力(expressive)

把map当成transform -- 耗子栏

    当我刚开始使用Scala和map方法的时候,我发现每次输入map时说成transform会很有帮助。也就是说,我希望这个方法被命名为transform而不是map

    fruits.map(_.capitalize)        // what it’s named
    fruits.transform(_.capitalize)  // what i wish it was named

    这是因为map将函数应用于初始列表中的每个元素,并将这些元素转换为一个新的列表。

    (正如我在博客“The ‘Great FP Terminology Barrier’ ”( https://oreil.ly/UX2rJ )中所解释的,map的名字来自于数学领域)。

13.6 用flatten对列表进行扁平化处理

问题

你已经有一个包含列表的列表(一个包含序列的序列),想根据它们创建一个列表(序列)。

解决方案

使用flatten方法把一个包含列表的列表转变为一个单列表。为了说明此点,首先创建一个列表的列表:

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

在列表的列表上调用flatten方法创建一个新列表:

    val x = lol.flatten // x: List(1, 2, 3, 4)

如上所示,flatten所做的正如其名,将外层列表中的两个列表扁平为一个结果列表。

虽然这里用了“list”这个词,但flatten方法并不局限于List,它也适用于其他序列(如ArrayArrayBufferVector等等)。

    val a = Vector(Vector(1,2), Vector(3,4))
    val b = a.flatten // b: Vector(1, 2, 3, 4)

讨论

在社交网络应用程序中,你可能对你的朋友和他们的朋友做着同样的事情:

    val myFriends = List("Adam", "David", "Frank")
    val adamsFriends = List("Nick K", "Bill M")
    val davidsFriends = List("Becca G", "Kenny D", "Bill M")
    val franksFriends: List[String] = Nil
    val friendsOfFriends = List(adamsFriends, davidsFriends, franksFriends)

因为friendsOfFriends是一个列表的列表:

    List(
        List("Nick K", "Bill M"),
        List("Becca G", "Kenny D", "Bill M"),
        List()
    )

你可以使用flatten来完成许多任务,比如创建一个独有的朋友的朋友列表:

    scala> val uniqueFriendsOfFriends = friendsOfFriends.flatten.distinct
    uniqueFriendsOfFriends: List[String] = List(Nick K, Bill M, Becca G, Kenny D)

Seq[Option]进行扁平化

当你有一个 Option 值的列表时,flatten 特别有用。因为 Option 可以被认为是一个持有零或一元素的容器,所以 flatten 对包含 SomeNone 元素的序列非常有用。因为Some类似于只有一个元素的列表,而None类似于没有元素的列表,flatten 在创建一个新的列表时,从Some元素中提取值并删除None元素:

    val x = Vector(Some(1), None, Some(3), None)    // x: Vector[Option[Int]]
    val y = x.flatten                               // y: Vector(1, 3)

如果你是刚刚接触Option/Some/None值,把包含SomeNone值的列表看成列表的列表,每个列表都包含一个或零个元素,会很有帮助:

    List( Some(1), None, Some(2) ).flatten      // List(1, 2)
    List( List(1), List(), List(2) ).flatten    // List(1, 2)

参考24.6小节,“使用Scala的错误处理类型(OptionTryEither)”,了解更多关于使用Option的内容。

组合map和flatMap的flatten

如果需要在一个序列上调用map,然后调用flatten,可以用flatMap代替。例如,下面有一个nums列表和返回一个Option的方法:

    val nums = List("1", "2", "three", "4", "one hundred")

    import scala.util.{Try,Success,Failure}
    def makeInt(s: String): Option[Int] = Try(Integer.parseInt(s.trim)).toOption

你可以使用mapflatten来计算列表中字符串正确转换为整数的总和:

    nums.map(makeInt).flatten // List(1, 2, 4)

然而,当你处理这样的列表时,可以使用flatMap来代替:

    nums.flatMap(makeInt) // List(1, 2, 4)

这总让我觉得这个方法应该叫做“map flat”,但flatMap这个名字已经存在很久了,而这只是它的一种可能的用法。

13.7 用filter对列表进行过滤

问题

你想过滤集合中的一些元素从而创建一个新集合,新集合只包含符合过滤条件的元素。

解决方案

要过滤一个序列:

  • 在不可变集合上使用filter方法。
  • 在可变集合上使用filterInPlace方法。

按需采用,也可以使用13.1小节中的其他方法来过滤一个集合。

不可变集合上使用filter方法

这是Seqfilter方法的签名:

    def filter(p: (A) => Boolean): Seq[A] // general case

因此,作为一个具体的例子,当你有一个Seq[Int] 时,签名是这样的:

    def filter(p: (Int) => Boolean): Seq[Int] // specific case for Seq[Int]

这意味着filter接收一个predicate--一个返回true或者false的函数,并返回一个序列。提供的谓词应该接受一个输入参数,其类型是序列元素的类型,并返回一个 Boolean 值。对于希望保留在新集合中的元素,函数应返回true,而对于希望删除的元素,函数应返回false。记住要把过滤操作的结果赋值给一个新的变量。

例如,下面的例子展示了,如何用一个整数列表和两种不同的算法来使用过滤器:

    val a = List.range(1, 10)           // a: List(1, 2, 3, 4, 5, 6, 7, 8, 9)

    // create a new list of all elements that are less than 5
    val b = a.filter(_ < 5)             // b: List(1, 2, 3, 4)
    val b = a.filter(e => e < 5)        // b: List(1, 2, 3, 4)

    // create a list of all the even numbers in the list
    val evens = x.filter(_ % 2 == 0)    // evens: List(2, 4, 6, 8)

如上所示,当函数/谓词被调用时,filter返回序列中所有返回true的元素。还有一个filterNot方法,从列表中返回函数结果为false的所有元素。

可变集合上使用filterInPlace方法

当你有一个像ArrayBuffer这样的可变集合时,使用filterInPlace而不是filter

    import scala.collection.mutable.ArrayBuffer
    val a = ArrayBuffer.range(1,10) // ArrayBuffer(1, 2, 3, 4, 5, 6, 7, 8, 9)

    a.filterInPlace(_ < 5)          // a: ArrayBuffer(1, 2, 3, 4)
    a.filterInPlace(_ > 2)          // a: ArrayBuffer(3, 4)

因为ArrayBuffer是可变的,不必把filterInPlace的结果赋值给另一个变量,变量a的内容直接被修改了。

讨论

用来过滤集合的主要方法在13.1小节中列出,为了方便起见,这里在提一下:collectdiffdistinctdropdropRightdropWhilefilterfilterNotfilterInPlacefindfoldLeftfoldRightheadheadOptioninitintersectlastlastOptionslicetailtaketakeRighttakeWhileunion

与其他方法相比,filter(和filterInPlace)的独特特性包括:

  • filter会遍历集合中的所有元素;其他一些方法会提前结束。
  • filter允许使用predicate来过滤元素。

Predicate控制过滤

如何过滤集合中的元素取决于算法。使用不可变的集合和过滤器,接下来的例子展示了几种过滤字符串列表的方法:

    val fruits = List("orange", "peach", "apple", "banana")
    val x = fruits.filter(f => f.startsWith("a"))   // List(apple)
    val x = fruits.filter(_.startsWith("a"))        // List(apple)
    val x = fruits.filter(_.length > 5)             // List(orange, banana)

使用collect方法来过滤集合

collect方法是一个有趣的过滤方法。collect方法在IterableOnceOps 特质中定义,并且根据IterableOnceOps Scaladoc ( https://oreil.ly/VHuV0 ),collect构建了一个新的列表,“通过将偏函数应用于列表中定义函数的所有元素”。正因为如此,用一个case语句来过滤列表是一种很好的方式,正如下面的REPL例子所示:

    scala> val x = List(0,1,2)
    val x: List[Int] = List(0, 1, 2)

    scala> val y = x.collect{ case i: Int if i > 0 => i }
    val y: List[Int] = List(1, 2)

    scala> val x = List(Some(1), None, Some(3))
    val x: List[Option[Int]] = List(Some(1), None, Some(3))

    scala> val y = x.collect{ case Some(i) => i}
    val y: List[Int] = List(1, 3)

这些例子之所以有效,是因为(a)在13.4小节中所说的,case表达式创建了一个匿名函数,(b)collect 方法使用偏函数。注意,虽然这些例子展示了collect是如何工作的,但第二个例子中 List[Option] 的值用flatten更容易减少:

    scala> x.flatten
    val res0: List[Int] = List(1, 3)

与此相关的是,还有一个collectFirst方法,它把匹配到的第一个元素作为一个Option返回:

    scala> val firstTen = (1 to 10).toList
    val firstTen: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    scala> firstTen.collectFirst{case x if x > 1 => x}
    val res0: Option[Int] = Some(2)

    scala> firstTen.collectFirst{case x if x > 99 => x}
    val res1: Option[Int] = None

13.8 从集合中提取元素序列

问题

你想从集合中提取一个元素序列,这可以通过指定一个起始位置和长度,也可以通过一个函数实现。

解决方案

这里有一些集合方法,这些方法可以从集合里提取元素序列,包括dropdropWhileheadheadOptioninitlastlastOptionslicetailtaketakeWhile

给定下面Vector

    val x = (1 to 10).toVector

drop方法从序列的开始删除指定的元素数量:

    val y = x.drop(3)           // y: Vector(4, 5, 6, 7, 8, 9, 10)

dropRight方法和drop方法一样,但是从集合的结尾处开始向前删除元素:

    val y = x.dropRight(4)      // y: Vector(1, 2, 3, 4, 5, 6)

dropWhile方法会删除当谓语为真时的元素:

    val y = x.dropWhile(_ < 6)  // y: Vector(6, 7, 8, 9, 10)

take方法从序列中提取前N个元素:

    val y = x.take(3)           // y: Vector(1, 2, 3)

takeRighttake工作方式相同,但从序列的末端提取元素:

    val y = x.takeRight(3)      // y: Vector(8, 9, 10)

takeWhile返回让谓词为真的元素:

    val y = x.takeWhile(_ < 5)  // y: Vector(1, 2, 3, 4)

性能说明 TODO -- 耗子图

       由于List类的构造方式,像dropRighttakeRight这样的方法在List这样的线性序列上执行得很慢。如果你需要对大的序列使用这些方法,使用像Vector这样的索引序列来代替。

slice(from, until) 返回从fromuntil下标的序列,不包括until,可以认为是基于0的下标:

    val chars = Vector('a', 'b', 'c', 'd')

    chars.slice(0,1) // Vector(a)
    chars.slice(0,2) // Vector(a, b)

    chars.slice(1,1) // Vector()
    chars.slice(1,2) // Vector(b)
    chars.slice(1,3) // Vector(b, c)

    chars.slice(2,3) // Vector(c)
    chars.slice(2,4) // Vector(c, d)

所有这些方法都提供了另一种过滤序列的方法,一个很明显的特点是它们返回的是连续的元素序列。

讨论

其实还有更多可以用的方法。给定一个列表:

    val nums = Vector(1, 2, 3, 4, 5)

以下表达式后面的注释展示了每个方法返回的值:

    nums.head       // 1
    nums.headOption // Some(1)
    nums.init       // Vector(1, 2, 3, 4)

    nums.tail       // Vector(2, 3, 4, 5)
    nums.last       // 5
    nums.lastOption // Some(5)

一般来说,这些方法的工作方式从它们的名字中就可以看出来,但有两个可能需要解释一下,那就是inittailinit方法返回序列中除了最后一个元素外的所有元素,tail方法返回除了第一个元素外的所有元素。

需要注意的是,headinittaillast会在空序列上抛出java.lang.UnsupportedOperationException异常。在函数式编程中,纯函数是全面的 —— 意味着它对每个输入都有定义,并且不抛出异常,因此纯函数式程序员通常不使用这些方法。当他们使用时,会仔细检查序列是否为空。

13.9 将序列拆分成子集

问题

根据定义的算法或位置,你希望将一个序列拆分为两个或多个序列,它们是初始序列的子集。

解决方案

使用groupBysplitAtpartitionspan方法将一个序列拆分成子序列。这些方法展示在下面的例子中。

groupBy

groupBy方法根据谓词函数将一个序列拆分成子集:

    val xs = List(15, 10, 5, 8, 20, 12)
    val groups = xs.groupBy(_ > 10)
        // Map(false -> List(10, 5, 8), true -> List(15, 20, 12))

groupBy根据你的函数将集合划分为包含N个序列的Map。在这个例子中有两个结果序列,但是根据你的算法,你可能会在一个map中得到任意数量的序列。

在这个特殊的例子中,true键引用谓词返回true的元素,而false键引用返回false的元素。groupBy创建的Map中的序列可以这样访问:

    val listOfTrues = groups(true) // List(15, 20, 12)
    val listOfFalses = groups(false) // List(10, 5, 8)

下面是有另一种算法,展示了通过groupBy创建多个列表:

    val xs = List(1, 3, 11, 12, 101, 102)

    // you can also use 'scala.math.log10' for this algorithm
    def groupBy10s(i: Int): Int =
        assert(i > 0)
        if i < 10 then 1
        else if i < 100 then 10
        else 100

    xs.groupBy(groupBy10s)  // result: HashMap(
                            // 1 -> List(1, 3),
                            // 10 -> List(11, 12),
                            // 100 -> List(101, 102)
                            // )

splitAt、partition、span

splitAt通过提供索引来拆分初始序列,从而从一个初始序列中创建两个子序列:

    val xs = List(15, 5, 20, 10)

    val ys = xs.splitAt(1)  // ys: (List(15), List(5, 20, 10))
    val ys = xs.splitAt(2)  // ys: (List(15, 5), List(20, 10))

partitionspan通过谓词函数将一个序列拆分为tuple-2(二元组)中的两个序列:

    val xs = List(15, 5, 25, 20, 10)

    val ys = xs.partition(_ > 10)   // ys: (List(15, 25, 20), List(5, 10))
    val ys = xs.partition(_ < 25)   // ys: (List(15, 5, 20, 10), List(25))

    val ys = xs.span(_ < 20)        // ys: (List(15, 5), List(25, 20, 10))
    val ys = xs.span(_ > 10)        // ys: (List(15), List(5, 25, 20, 10))

splitAtpartitionspan方法创建序列的tuple-2(二元组),其类型与原始集合相同。它们是这样工作的:

  • splitAt根据提供的元素索引值拆分初始序列。
  • partition创建两个列表,一个包含谓词返回true的元素,另一个包含返回false的元素。
  • span返回基于谓词ptuple-2(二元组),包括“这个列表中元素都满足p的最长前缀部分,以及这个列表的其余部分。”

当一个序列的tuple-2(二元组)返回时,两个序列可以这样访问:

    scala> val (a,b) = xs.partition(_ > 10)
    val a: List[Int] = List(15, 20)
    val b: List[Int] = List(5, 10)

讨论

虽然这些是我最常使用的分组方法,但还有其他方法,包括slidingunzip

sliding

sliding(size,step) 方法很有趣,可以将序列分成多个组。调用可以只使用size参数,也可以同时使用sizestep

    val a = Vector(1,2,3,4,5)

    // size = 2
    val b = a.sliding(2).toList     // b: List(Array(1, 2), Array(2, 3),
                                    //         Array(3, 4), Array(4, 5))

    // size = 2, step = 2
    val b = a.sliding(2,2).toList   // b: List(Vector(1, 2), Vector(3, 4),
                                    //         Vector(5))

    // size = 2, step = 3
    val b = a.sliding(2,3).toList   // b: List(Vector(1, 2), Vector(4, 5))

如上所示,sliding的工作方式是在初始序列上传递一个“滑动窗口”,返回长度为size大小的序列。step参数允许跳过元素,如最后两个例子所示。

unzip

unzip方法也很有趣。它可以用来获取一个tuple-2(二元组)值的序列,并创建两个结果列表:一个包含每个元组的第一个元素,另一个包含每个元组的第二个元素:

    val listOfTuple2s = List((1,2), ('a','b'))
    val x = listOfTuple2s.unzip // (List(1, a),List(2, b))

例如,给定许多夫妻名字,你可以用unzip来创建一个女人列表和一个男人列表:

    val couples = List(("Wilma", "Fred"), ("Betty", "Barney"))
    val (women, men) = couples.unzip    // val men: List(Fred, Barney)
                                        // val women: List(Wilma, Betty)

正如你可能从它的名字中猜到的那样,unzip是与zip相反的:

    val couples = women zip men         // List((Wilma,Fred), (Betty,Barney))

更多的方法请参阅序列(ListVector等)的Scaladoc。

13.10 用reduce和fold方法遍历集合

问题

你想遍历一个序列中的所有元素,并对所有相邻的元素使用一种算法,然后从操作中获取一个标量的结果,如总和、乘积、最大值、最小值等等。

解决方案

reduceLeftfoldLeftreduceRightfoldRight方法来遍历序列中的元素,并将自定义算法应用于元素,产生一个标量的结果。解决方案中展示了reducleft,其他方法在讨论中提到(如scanLeftscanRight方法,返回一个序列作为最后结果)。

例如,使用reducleft从左到右遍历一个序列,从第一个元素开始到最后一个元素结束。根据IterableOnceOps 特质的Scaladoc( https://oreil.ly/ujux2 )(这些方法在特质中定义的),传入一个associative binary operator,然后reduceLeft首先将操作符(算法)应用到序列中的前两个元素,从而创建一个中间结果。接下来,将算法应用到中间结果和第三个元素,应用程序会产生一个新的结果。这个过程一直持续到序列的末尾。

如果之前没有用过这些方法,会发现这些方法是非常强大的。展示这点最好的办法是举例说明。首先,创建一个示例集合:

    val xs = Vector(12, 6, 15, 2, 20, 9)

假定有这样一个序列,用reduceLeft来确定集合的不同属性。接下来的例子展示了如何求集合所有元素总和的方法:

    val sum = xs.reduceLeft(_ + _) // 64

不要被下划线吓到,它们只是代表了传给reduceLeft函数的两个参数。如果愿意,也可以像这样写匿名函数:

    xs.reduceLeft((x,y) => x + y)

下面的例子展示了如何使用reduceLeft得到序列中所有元素的乘积,最小值以及最大值:

    xs.reduceLeft(_ * _)    // 388800
    xs.reduceLeft(_ min _)  // 2
    xs.reduceLeft(_ max _)  // 20

注意如果元素只包含一个元素,reduceLeft也是有效的:

    List(1).reduceLeft(_ * _)   // 1
    List(1).reduceLeft(_ min _) // 1
    List(1).reduceLeft(_ max _) // 1

然而,如果给定的序列为空,则抛出异常:

    val emptyVector = Vector[Int]()
    val sum = emptyVector.reduceLeft(_ + _)
    // result: java.lang.UnsupportedOperationException: empty.reduceLeft

由于这种异常行为,在使用reduceLeft之前要检查集合的大小,或者使用foldLeft,在第一个参数中提供一个初始种子值,从而避免这个问题:

    val emptyVector = Vector[Int]()
    emptyVector.foldLeft(0)(_ + _)  // 0
    emptyVector.foldLeft(1)(_ * _)  // 1

讨论

可以通过创建一个使用reduceLeft的方法来展示它是如何工作的。下面的函数做了一个最大值的比较,就像上一个例子,但加了一些调试代码,可以方便地在reduceLeft遍历序列时看到它是如何工作的。函数如下:

    def findMax(x: Int, y: Int): Int =
        val winner = x max y
        println(s"compared $x to $y, $winner was larger")
        winner

在序列上再次调用reduceLeft,这次传给它findMax方法:

    scala> xs.reduceLeft(findMax)
    compared 12 to 6, 12 was larger
    compared 12 to 15, 15 was larger
    compared 15 to 2, 15 was larger
    compared 15 to 20, 20 was larger
    compared 20 to 9, 20 was larger
    res0: Int = 20

输出展示了reduceLeft是如何遍历序列元素的,以及每一步是如何调用函数的。具体步骤如下:

  • reduceLeft先调用findMax来测试数组中的前两个元素,126findMax返回12,因为126大。
  • reduceLeft得到这个结果(12)并调用 findMax(12, 15)12是第一次比较的结果,15是下一个元素,15更大,所以15变成了新结果。
  • reduceLeft会保留函数返回的结果,然后用它与集合中的下一个元素进行比较,直到遍历结束,得到最终结果20

减法算法

我在这个小节中提到,“left”方法(reduceLeftfoldLeft)从集合的开始移动到末尾,而“right”方法则从集合的末尾开始移动到开始。展示这一点的好方法是使用减法算法,由于减法是不可交换,因此当从左到右或从右到左遍历列表时,会产生不同的结果。

例如,下面列表和减法算法:

    val xs = List(1, 2, 3)

    def subtract(a: Int, b: Int): Int =
        println(s"a: $a, b: $b")
        val result = a - b
        println(s"result: $result\n")
        result

REPL展示了reduceLeftreduceRight如何工作:

    scala> xs.reduceLeft(subtract)
    a: 1, b: 2
    result: -1

    a: -1, b: 3
    result: -4

    val res0: Int = -4

    scala> xs.reduceRight(subtract)
    a: 2, b: 3
    result: -1

    a: 1, b: -1
    result: 2

    val res1: Int = 2

如上所示,reduceLeftreduceRight在减法算法中产生不同的结果,因为前者从左到右遍历列表,后者从右到左遍历列表。

编写reduce算法 --- TODO 耗子栏

关于reduce方法有一个微妙但重要的注意事项:提供的自定义函数必须返回和集合中相同的数据类型。这是必要的,以便reduce算法可以将函数的结果与集合中的下一个元素进行比较。

foldLeft、reduceRight和foldRight

foldLeft方法如同reduceLeft,但它会设置一个种子值用于第一个元素。下面的例子展示了一个求总和算法,先用reduceLeft再用foldLeft,以示区别:

    val xs = List(1, 2, 3)

    xs.reduceLeft(_ + _) // 6
    xs.foldLeft(20)(_ + _) // 26
    xs.foldLeft(100)(_ + _) // 106

在上两个例子中,foldLeft20接着是100作为第一个元素,这种做法会影响到最终结果。

如果之前没有见过这样的语法,foldLeft接收两个参数列表。第一个参数表示初始的种子值。第二个参数是要运行的代码块(你的算法)。4.17小节“创建自己的控制结构”说明了多个参数列表的用法。

reduceRightfoldRight方法与reduceLeftfoldLeft方法类似,但它们从集合的末尾开始,从右向左,直到开头。

fold算法和标识值

fold算法与列表类型的标识值和列表上执行的操作有关。例如:

  • 操作 Seq[Int] 时,加0对加法和求和算法没有影响。
  • 同样地,1对乘积算法没有影响。
  • 操作 Seq[String] 时,空字符串 "",对加法或乘积算法没有影响。

因此,你可能会看到fold是这样写的:

    listOfInts.foldLeft(0)(_ + _)
    listOfInts.foldLeft(1)(_ * _)

    // concatenate a list of strings:
    listOfStrings.foldLeft("")(_ + _)

使用这些值可以使用空列表的fold方法。我在“Tail-Recursive Algorithms in Scala” ( https://oreil.ly/HTJpY )中写了更多关于标识值的内容。

fold和reduce方法工作方式的总结

下面是要关于foldreduce方法的关键点:

  • 它们遍历整个序列。
  • 如最大值与最小值算法所示,比较的是序列中的相邻元素。
  • 如求和与乘积算法所示,遍历序列时要创建一个累加器。
  • fold和reduce算法一般用于在最后产生一个单一的标量值(而不是另一个序列)。
  • 自定义函数必须接收集合中所包含类型的两个参数,并且必须返回相同的类型。

scanLeft和scanRight

scanLeftscanRight这两个方法遍历序列的方式与reduceLeftreduceRight类似,但它们返回一个序列而非一个单个值。

例如,根据Scaladoc,scanLeft“生成一个集合,其中包含从左到右应用操作符的累积结果”。为了理解它是如何工作的,在它里面新建一个带调试代码的函数:

    def product(x: Int, y: Int): Int =
        val result = x * y
        println(s"multiplied $x by $y to yield $result")
        result

下面是使用了上面函数和一个种子值时的scanLeft输出:

    scala> val xs = Vector(1, 2, 3)
    xs: Vector[Int] = Vector(1, 2, 3)

    scala> xs.scanLeft(10)(product)
    multiplied 10 by 1 to yield 10
    multiplied 10 by 2 to yield 20
    multiplied 20 by 3 to yield 60
    res0: Vector[Int] = Vector(10, 10, 20, 60)

如上所述,scanLeft返回一个新序列,而不是单个值。scanRight方法方式是一样的,但遍历方向是从右到左。

参阅序列的Scaladoc,了解更多的相关方法,包括reducereduceLeftOptionreduceRightOption

另见

  • 如果你想要了解更多细节和说明,我在 “Recursion Is Great, but Check out Scala’s fold and reduce!” ( https://oreil.ly/Vmlfp )中深入研究foldreduce方法。

13.11 从序列中查找不重复的元素

问题

你有一个包含重复元素的序列,你想移除重复的元素,只留下唯一的元素。

解决方案

可以调用序列的distinct方法,也可以调用toSet

    val x = Vector(1, 1, 2, 3, 3, 4)
    val y = x.distinct // Vector(1, 2, 3, 4)
    val z = x.toSet // Set(1, 2, 3, 4)

这两种方法都会返回一个新的序列,并删除重复的值,但是distinct返回与开始时相同的序列类型,而toSet会返回一个Set

讨论

要在自己的类中使用这些方法,需要实现equalshashCode方法。例如,样例类为你实现了这些方法,所以你可以将Person类与distinct一起使用:

    case class Person(firstName: String, lastName: String)

    val dale1 = Person("Dale", "Cooper")
    val dale2 = Person("Dale", "Cooper")
    val ed = Person("Ed", "Hurley")
    val list = List(dale1, dale2, ed)

    // correct solution: only one Dale Cooper appears in this result:
    val uniques = list.distinct // List(Person(Dale,Cooper), Person(Ed,Hurley))
    val uniques = list.toSet    // Set(Person(Dale,Cooper), Person(Ed,Hurley))

如果你不想使用样例类,参阅5.9小节,“定义equals方法(对象相等性)”,讨论了如何实现equalshashCode方法。

13.12 合并序列集合

问题

你想把两个序列合并为一个序列,要么保留所有的原始元素,找到两个集合的共同元素,要么找到两个集合的不相同元素。

解决方案

这个问题很多解决方案,具体情况根据需求:

  • 使用 ++concat方法将两个可变或不可变序列的所有元素合并成一个新的序列。
  • 使用 ++= 方法将一个序列的元素合并到一个已有的可变序列。
  • 使用 ::: 将两个列表合并为一个新的列表。
  • 使用intersect方法找出两个序列的交集。
  • 使用diff方法找出两个序列的差异。

正如接下来的例子所示,你也可以使用distincttoSet来将序列缩减为只包含唯一元素。

++ 或 concat方法

++ 方法是 concat 方法的一个别名,所以使用它们中的任何一个来合并两个可变或不可变的序列,同时将结果分配给一个新的变量:

    val a = List(1,2,3)
    val b = Vector(4,5,6)

    val c = a ++ b      // c: List[Int] = List(1, 2, 3, 4, 5, 6)
    val c = a.concat(b) // c: List[Int] = List(1, 2, 3, 4, 5, 6)

    val d = b ++ a      // d: Vector[Int] = Vector(4, 5, 6, 1, 2, 3)
    val d = b.concat(a) // d: Vector[Int] = Vector(4, 5, 6, 1, 2, 3)

++=方法

使用 ++= 方法将一个序列合并到一个已有可变的序列中,比如ArrayBuffer

    import collection.mutable.ArrayBuffer

    // merge sequences into an ArrayBuffer
    val a = ArrayBuffer(1,2,3)
    a ++= Seq(4,5,6) // a: ArrayBuffer(1, 2, 3, 4, 5, 6)
    a ++= List(7,8)  // a: ArrayBuffer(1, 2, 3, 4, 5, 6, 7, 8)

使用:::合并两个列表

如果你碰巧在处理一个列表,使用 ::: 方法将一个列表的元素前加到另一个列表中,同时将结果分配给一个新的变量:

    val a = List(1,2,3,4)
    val b = List(4,5,6,7)
    val c = a ::: b // c: List(1, 2, 3, 4, 4, 5, 6, 7)

intersect 和 diff

intersect方法找到两个序列的交点--两个序列的共同元素:

    val a = Vector(1,2,3,4,5)
    val b = Vector(4,5,6,7,8)

    val c = a.intersect(b) // c: Vector(4, 5)
    val c = b.intersect(a) // c: Vector(4, 5)

diff方法找到两个序列之间的差异 —— 那些在第一个序列中但不在第二个序列中的元素。由于这个定义,它的结果取决于它在哪个序列上被调用:

    val a = List(1,2,3,4)
    val b = List(3,4,5,6)
    val c = a.diff(b) // c: List(1, 2)
    val c = b.diff(a) // c: List(5, 6)

    val a = List(1,2,3,4,1,2,3,4)
    val b = List(3,4,5,6,3,4,5,6)
    val c = a.diff(b) // c: List(1, 2, 1, 2)
    val c = b.diff(a) // c: List(5, 6, 5, 6)

Scaladoc中说明diff方法会返回,“一个新列表,该列表包含this列表的所有元素,但不包含that中同样出现的元素。如果一个元素值xthat中出现了n次,那么这些元素都不会成为结果的一部分,但之后出现的会”。由于这种行为,你可能还需要使用 distinct 方法来创建不同元素的列表:

    val a = List(1,2,3,4,1,2,3,4)
    val b = List(3,4,5,6,3,4,5,6)

    val c = a.diff(b)           // c: List(1, 2, 1, 2)
    val c = b.diff(a)           // c: List(5, 6, 5, 6)

    val c = a.diff(b).distinct  // c: List(1, 2)
    val c = b.diff(a).distinct  // c: List(5, 6)

讨论

你可以用diff来获得两个集合的相对补集。集合A相对于集合B的相对补集就是在B中出现但不在A中的元素。

在最近的一个项目中,我需要找到在一个序列中但不在另一个序列中的那些唯一元素。我是这样做的,首先在两个序列上调用distinct,然后用diff来比较它们。例如,给定这两个Vector

    val a = Vector(1,2,3,11,4,12,4,4,5)
    val b = Vector(6,7,4,4,5)

找到每个Vector的相对补集的一种方法是,首先调用这个Vectordistinct方法,然后用diff将它与另一个Vector进行比较:

    // the elements in a that are not in b
    val uniqToA = a.distinct.diff(b) // Vector(1, 2, 3, 11, 12)

    // the elements in b that are not in a
    val uniqToB = b.distinct.diff(a) // Vector(6, 7)

如果需要,可以对这些结果求和,以得到第一个集合或第二个集合中的元素列表,但不是两个集合都有的:

    val uniqs = uniqToA ++ uniqToB // Vector(1, 2, 3, 11, 12, 6, 7)

在我研究这个问题的时候,还有另一种方法可以得到同样的结果:

    // create a list of unique elements that are common to both lists
    val i = a.intersect(b).toSet   // Set(4, 5)

    // subtract those elements from the original lists
    val uniqToA = a.toSet -- i     // HashSet(1, 2, 12, 3, 11)
    val uniqToB = b.toSet -- i     // Set(6, 7)

    val uniqs = uniqToA ++ uniqToB // HashSet(1, 6, 2, 12, 7, 3, 11)

注意,我在这个解决方案中使用toSet,因为它是从序列中创建唯一元素列表的另一种方法。

13.13 随机化序列

问题

你想随机化(shuffle)一个现有的序列,或者从序列中获得一个随机元素。

解决方案

要随机化一个序列,需要导入scala.util.Random,然后将其shuffle方法应用于现有的序列,同时将结果分配给一个新的序列:

    import scala.util.Random

    // List
    val xs = List(1,2,3,4,5)
    val ys = Random.shuffle(xs)                     // 'ys' will be shuffled, like List(4,1,3,2,5)

    // also works with other sequences
    val x = Random.shuffle(Vector(1,2,3,4,5))       // x: Vector(5,3,4,1,2)
    val x = Random.shuffle(Array(1,2,3,4,5))        // x: mutable.ArraySeq(1,3,2,4,5)

    import scala.collection.mutable.ArrayBuffer
    val x = Random.shuffle(ArrayBuffer(1,2,3,4,5))  // x: ArrayBuffer(4,2,3,1,5)

与函数式方法一样,这个解决方案的关键是知道shuffle不会随机化给定的列表。相反,它返回一个已经随机化了(shuffled)的新列表。

讨论

当你想对整个列表进行随机化时,这个解决方案非常有效。但如果你只想从列表中获得一个随机元素,下面的方法会更有效:

    import scala.util.Random

    // throws an IllegalArgumentException if `seq` is empty
    def getRandomElement[A](seq: Seq[A]): A =
        seq(Random.nextInt(seq.length))

只要你传入的序列至少包含一个元素,函数就能工作。它的工作原理是 nextInt 返回一个介于 0(包括)和 seq.length(不包括)之间的值。因此,如果序列包含100个元素,nextInt返回一个介于099之间的值,这与序列的索引相匹配。

现在只要你有一个序列,就可以用这个函数从序列中获取一个随机元素:

    val randomNumber = getRandomElement(List(1,2,3))
    val randomString = getRandomElement(List("a", "b", "c"))

13.14 集合排序

问题

你想(a)对一个序列集合进行排序,(b)在一个自定义类中实现Ordered特质,这样可以用排序方法,或者像 <<=>>= 操作符去比较类的实例,(c)在排序时使用隐式或显式的Ordering

解决方案

参考12.11小节,“对数组进行排序”,了解如何对数组进行排序。否则使用sortedsortWithsortBy方法来对不可变的序列进行排序,而 sortInPlacesortInPlaceWithsortInPlaceBy来对可变序列进行排序。 你可以根据自己需要实现OrderedOrdering特质。

不可变序列使用sorted、sortWith和sortBy

序列上的sorted方法可以对DoubleIntString以及其他任何具有隐式scala.math.Ordering类型的集合进行排序:

    List(10, 5, 8, 1, 7).sorted         // List(1, 5, 7, 8, 10)
    List("dog", "mouse", "cat").sorted  // List(cat, dog, mouse)

sortWith方法可以自己写排序算法。下面的例子展示如何对一个StringInt的序列进行两个方向的排序:

    // short form: sorting algorithm uses '_' references
    Vector("dog", "mouse", "cat").sortWith(_ < _)   // Vector(cat, dog, mouse)
    Vector("dog", "mouse", "cat").sortWith(_ > _)   // Vector(mouse, dog, cat)
    // long form: sorting algorithm uses a tuple-2
    Vector(10, 5, 8, 1, 7).sortWith((a,b) => a < b) // Vector(1, 5, 7, 8, 10)
    Vector(10, 5, 8, 1, 7).sortWith((a,b) => a > b) // Vector(10, 8, 7, 5, 1)

排序函数接收两个参数,简单或复杂看你需要。如果排序函数较长,首先可以把它声明成一个方法:

    def sortByLength(s1: String, s2: String): Boolean =
        println(s"comparing $s1 & $s2")
        s1.length > s2.length

然后传入sortWith方法:

    scala> val a = List("dog", "mouse", "cat").sortWith(sortByLength)
    comparing mouse & dog
    comparing cat & mouse
    comparing mouse & cat
    comparing cat & dog
    comparing dog & cat
    a: List[String] = List(mouse, dog, cat)

根据Scaladoc,当使用 sortBy 方法“排序该序列时,隐式参数given Ordering,以及转换函数f ,将构成一个符合当前序列元素类型的 Ordering 变量作为排序依据”。ListsortBy 签名看起来像这样,请留意元素类型的转换关系:

    def sortBy[B](f: (A) => B)(implicit ord: Ordering[B]): List[A]

下面有几个使用sortBy进行排序的例子:

    val a = List("peach", "apple", "pear", "fig")
    val b = a.sortBy(s => s.length)             // b: List(fig, pear, peach, apple)

    // the Scaladoc shows an example like this that works “because scala.Ordering
    // will implicitly provide an Ordering[Tuple2[Int, Char]]”
    val b = a.sortBy(s => (s.length, s.head))   // b: List(fig, pear, apple, peach)

    // a way to sort from the longest to the shortest string, and then
    // by the string
    val a = List("fin", "fit", "fig", "pear", "peas", "peach", "peat")
    val b = a.sortBy(s => (-s.length, s))
    // b: List(peach, pear, peas, peat, fig, fin, fit)

可变序列使用sortInPlace、sortInPlaceWith和sortInPlaceBy

ArrayBuffer这样的可变序列,你可以使用sortInPlacesortInPlaceWithsortInPlaceBy方法。如果序列中的数据类型支持隐式Ordering或实现Ordered来排序,sortInPlace是一个直接的解决方案:

    import scala.collection.mutable.ArrayBuffer

    val a = ArrayBuffer(3,5,1)
    a.sortInPlace   // a: ArrayBuffer(1, 3, 5)

    val b = ArrayBuffer("Mercedes", "Hannah", "Emily")
    b.sortInPlace   // b: ArrayBuffer(Emily, Hannah, Mercedes)

sortInPlaceWithsortWith类似:

    import scala.collection.mutable.ArrayBuffer
    val a = ArrayBuffer(3,5,1)
    a.sortInPlaceWith(_ < _)    // a: ArrayBuffer(1, 3, 5)
    a.sortInPlaceWith(_ > _)    // a: ArrayBuffer(5, 3, 1)

sortInPlaceBy的工作方式与sortBy类似,可以指定一个函数来排序:

    import scala.collection.mutable.ArrayBuffer
    val a = ArrayBuffer("kiwi", "apple", "fig")
    a.sortInPlaceBy(_.length)   // a: ArrayBuffer(fig, kiwi, apple)

讨论

下面的讨论展示了如何在immutable序列中使用OrderingOrdered,讨论也适用于mutable序列。

有隐式的Ordering

如果一个序列所持有的类型缺少隐式Ordering,将无法用sorted方法排序。例如,下面这个Person类和List[Person]

    class Person(val name: String):
        override def toString = name

    val dudes = List(
        Person("Bill"),
        Person("Al"),
        Person("Adam")
    )

如果试图在 REPL 中对这个列表进行排序,你会看到一个错误,Person类没有隐式Ordering

    scala> dudes.sorted
    1 |dudes.sorted
      |           ^
      |           No implicit Ordering defined for B
      |           where: B is a type variable with constraint >: Person
      |           I found:
      |               scala.math.Ordering.ordered[A](/* missing
      |               */summon[scala.math.Ordering.AsComparable[B]])
      |           But no implicit values were found that match type
      |           scala.math.Ordering.AsComparable[B].

所以你不能排序Person类,解决方案是写一个匿名函数,使用sortWithPerson元素的name字段进行排序:

    dudes.sortWith(_.name < _.name) // List(Adam, Al, Bill)
    dudes.sortWith(_.name > _.name) // List(Bill, Al, Adam)

给sorted提供一个显式的Ordering

如果你的类没有隐式Ordering,解决方案是提供一个显式的Ordering。例如,在默认情况下,下面Person类没有提供任何关于如何排序的信息:

    class Person(val firstName: String, val lastName: String):
        override def toString = s"$firstName $lastName"

所以试图用sorted对列表中的Person实例排序是不行,正如刚才所展示的:

    val peeps = List(
        Person("Jessica", "Day"),
        Person("Nick", "Miller"),
        Person("Winston", "Bishop")
    )

    scala> peeps.sorted
    1 |peeps.sorted
      |           ^
      |No implicit Ordering defined for B ... (long error message) ...

一个解决方案是给Person创建一个显式的Ordering

    object LastNameOrdering extends Ordering[Person]:
        def compare(a: Person, b: Person) = a.lastName compare b.lastName

现在使用LastNameOrdering结合sorted,排序如愿所偿:

    scala> val sortedPeeps = peeps.sorted(LastNameOrdering)
    val sortedPeeps: List[Person] = List(Winston Bishop, Jessica Day, Nick Miller)

这个解决方案是可行的,因为(a)sorted被定义为接收一个隐式Ordering参数:

    def sorted[B >: A](implicit ord: Ordering[B]): List[A]
                      --------------------------

并且(b)显式提供该参数:

    val sortedPeeps = peeps.sorted(LastNameOrdering)
                                    ----------------

另一种解决方案是用implicit关键字声明LastNameOrdering,然后调用sorted

    implicit object LastNameOrdering extends Ordering[Person]:
        def compare(a: Person, b: Person) = a.lastName compare b.lastName

    val sortedPeeps = peeps.sorted
        // sortedPeeps: List(Winston Bishop, Jessica Day, Nick Miller)

在这个解决方案中,因为LastNameOrdering通过implicit关键字定义的,所以它被神奇地拉进来,用作sorted正在寻找的隐式Ordering参数。

混入Ordered特质来排序

如果你想使用带有sorted方法的Person类,另一个解决方案是将Ordered特质混入Person,然后实现Ordered特质的抽象compare方法。下面代码展示了这种技术:

    class Person(var name: String) extends Ordered[Person]:
        override def toString = name
            // return 0 if the same, negative if this < that, positive if this > that
            def compare(that: Person): Int =
                // depends on the definition of `==` for String
                if this.name == that.name then
                    0
                else if this.name > that.name then
                    1
                else
                    -1

现在,Person类可以通过sorted排序:

    val dudes = List(
        Person("Bill"),
        Person("Al"),
        Person("Adam")
    )

    val x = dudes.sorted // x: List(Adam, Al, Bill)

Ordered中的compare方法是抽象方法,所以需要在自己的类中实现它,从而提供排序能力。下面的说明展示了compare方法是如何工作的:

  • 对象相等返回0(你可以随意自定义相等,但通常使用类的equals方法):
  • thisthat小,返回负值。
  • thisthat大,返回正值。

如何确定一个实例是否大于另一个实例,完全取决于你的compare算法。

注意因为这种compare算法只对两个String值进行比较,所以可以这样写:

    def compare (that: Person) = this.name.compare(that.name)

然而,我按照第一个例子中的写法来写,可以更加清楚地了解这个方法。

在类中混入Ordered特质的一个额外好处是,可以在代码中直接比较对象实例:

    val bill = Person("Bill")
    val al = Person("Al")
    val adam = Person("Adam")

    if adam > bill then println(adam) else println(bill)

这是因为Ordered特质实现了 <=<>>= 方法,它们调用compare方法来进行比较。

另见

要了解更多内容,OrderedOrdering 的Scaladoc很棒,有些不错的例子。

13.15 通过mkString和addString将集合转换成字符串

问题

你想把集合的元素转成一个String,可能是添加一个字段分隔符、前缀和后缀。

解决方案

使用mkStringaddString方法,将集合打印成一个String

mkString

给定一个简单的集合:

    val x = Vector("apple", "banana", "cherry")

你可以使用mkString将元素转换为一个String

    x.mkString // "applebananacherry"

看起来并不是很有用,所以加一个分隔符:

    x.mkString(" ")     // "apple banana cherry"
    x.mkString("|")     // "apple|banana|cherry"
    x.mkString(", ")    // "apple, banana, cherry"

mkString被重载了,所以你可以在创建一个字符串时添加前缀和后缀:

    x.mkString("[", ", ", "]") // "[apple, banana, cherry]"

Map类上也有一个mkString方法:

    val a = Map(1 -> "one", 2 -> "two")
    a.mkString                      // "1 -> one2 -> two"
    a.mkString("|")                 // "1 -> one|2 -> two"
    a.mkString("| ", " | ", " |")   // "| 1 -> one | 2 -> two |"

addString

从Scala 2.13开始,新的addString方法类似于mkString,但可以让你用序列的内容填充一个可变的StringBuilder。和mkString一样,你可以单独使用addString,也可以使用一个分隔符,还可以使用开始,结束和分隔符:

    val x = Vector("a", "b", "c")

    val sb = StringBuilder()
    val y = x.addString(sb)         // y: StringBuilder = abc

    val sb = StringBuilder()
    val y = x.addString(sb , ", ")  // y: StringBuilder = "a, b, c"

    val sb = StringBuilder()
    val y = x.addString(
        sb,     // StringBuilder
        "[",    // start
        ", ",   // separator
        "]"     // end
    )

    // result of the last expression:
    y: StringBuilder = [a, b, c]
    y(0) // Char = '['
    y(1) // Char = 'a'

因为该技术使用的是StringBuilder而不是String,所以它在处理大数据集时会更快。(通常总是测试任何与性能相关的问题。)

讨论

上面这些技术创建的字符串是基于整合序列中元素的字符串表达,也就是说,通过调用元素的 toString 方法获得其对应的字符串表达。因此,该技术对字符串和整数这样的类型很有效,但假设你有一个没有实现 toString 方法的 Person 类,然后把 Person 放到 List 中,此时得到的字符串不太具备适合表达任何意思:

    class Person(val name: String)
    val xs = List(Person("Schmidt"))
    xs.mkString // Person@1b17b5cb (not a useful result)

为了解决这个问题,需要在类中实现toString方法:

    class Person(val name: String):
        override def toString = name

    List(Person("Schmidt")).mkString    // "Schmidt"

从重复字符创建字符串

稍微说明下,你可以通过下面技术用CharString填充一个序列:

    val list = List.fill(5)('-')          // List(-, -, -, -, -)

然后你可以把这个列表转换为一个String

    val list = List.fill(5)('-').mkString // "-----"

我还采取过另一种方法为一个句子生成下划线。比如,当知道一个句子的长度是10个字符时,你就会用刚才的例子创建10个下划线字符。但,其实有个更简单的技术:把所需字符与长度相乘,则可创建一个字符串,如下所示:

    "\u2500" * 10 // String = ──────────

关于这些技术,我在“Scala Functions to Repeat a Character n Times (Blank Padding)”( https://oreil.ly/tYC5y )有更多介绍。