转载

Scala Collections 提示和技巧

原文: Scala Collections Tips and Tricks ,

作者 Pavel Fatin 是JetBrains 的一名员工,为神器IntelliJ IDEA开发Scala插件。

受其工作 Scala Collections inspections )的启发,他整理了这个关于Java Collections API技巧的列表。

一些技巧只在一些微妙的实现细节中体现,但是大部分技巧都是一般的常识,但是在大部分情况下被忽视了。

性和谐提示和技巧很有价值,可以帮你深入理解Scala Collections,可以是你的代码更快更简洁。

图例 Legend

为了使下面的例子更容易理解,这里列出了一些约定:

  • seq — 一个 Seq -based的集合, 比如 Seq(1, 2, 3)
  • set — 一个 Set 实例, 比如 Set(1, 2, 3)
  • array — 一个数组, 比如 Array(1, 2, 3)
  • option — 一个 Option , 比如 Some(1)
  • map — 一个 Map , 比如 Map(1 -> "foo", 2 -> "bar")
  • p — 一个断言 predicate 函数,类型 T => Boolean , 比如 _ > 2
  • n — 一个整数
  • i — 一个索引
  • f, g — 简单函数, A => B
  • x, y — 一些字面值(arbitrary values)
  • z — 初始值或者缺省值

组合Composition

记住,尽管这些技巧都是独立和自包含的,我们还是可以将它们组合起来逐步迭代得到一个更高级的表达式,比如下面的例子,在一个Seq中检查一个元素是否存在:

seq.filter(_ == x).headOption != None

// Via seq.filter(p).headOption -> seq.find(p)

seq.find(_ == x) != None

// Via option != None -> option.isDefined

seq.find(_ == x).isDefined

// Via seq.find(p).isDefined -> seq.exists(p)

seq.exists(_ == x)

// Via seq.exists(_ == x) -> seq.contains(x)

seq.contains(x)

我们可以依赖”substitution model of recipe application“ ( SICP) )来简化复杂的表达式。

副作用 Side effects

”Side effects“是一个基础概念,在函数式编程语言中会有这个概念。 Scala有一个PPT专门介绍: Side-effect checking for Scala

基本上,”Side effects“是这样一个动作, 除了返回一个值外,外部函数或者表达式还观察到此动作还有以下行为之一:

  • 有输入输出操作 (比如文件,网络I/O)
  • 对外部变量有修改
  • 外部对象的状态有改变
  • 抛出异常

当一个函数或者表达式有以上任何一种情况时,我们就说它有副作用(Side effects),否则我们就说它是"纯"的函数或者表达式 (pure)。

side effects有什么大不了的?当有副作用时,计算的顺序不能随便改变。 比如下面两个"纯" (pure)表达式:

val x = 1 + 2
val y = 2 + 3

因为它们没有副作用,两个表达式可以互换位置,先 xy 和先 yx 的效果一样。

如果有副作用 (有控制台输出):

val x = { print( "foo" ); 1 + 2 }

val y = { print( "bar" ); 2 + 3 }

两个表达式不能互换位置,因为一旦互换位置,输出结果的顺序变了。

所以副作用的一个影响就是会减少可能的转换的数量(reduces the number of possible transformations),包括可能的简化和优化。

同样的原因可适用用Collection相关的表达式上。看一个外部的 builder 变量(副作用方法 append ):

seq filter { x => builder.append(x); x > 3 } headOption

原则上 seq.filter(p).headOption 可以简化为 seq.find(p) ,但是副作用阻止我们这么做. 如果你尝试这么做:

seq find { x => builder.append(x); x > 3 }

结果和前一个表达式并不一样。 前一个表达式计算后所有大于3的元素都增加到 builder 中了, 后一个表达式在找到第一个大于3的元素后就不会再增加了。

两个表达式并不等价。

自动简化是否可能?这里有两条黄金法则,可以用在有副作用的代码上:

  1. 尽可能的避免副作用
  2. 否则将副作用嗲吗从纯代码中分离开

对于上面的例子,我们需要去掉 builder 或者将它从纯代码中隔离。 考虑到 builder 是第三方的对象,我们无法去除,那我们通过隔离的方式实现:

seq.foreach(builder.append)

seq.filter(_ > 3 ).headOption

这样我们就可以用到本文中的技巧进行替换:

seq.foreach(builder.append)

seq.find(x > 3 )

干的漂亮!自动简化也成为可能,一个额外好处是由于清晰的隔离,代码更容易理解。

一个不太明显的好处是,代码变得更健壮。如上面的例子,副作用针对不同的 Seq 实现,副作用的结果也不相同, 比如 VectorStream , 副作用隔离可以让我们避免这种不确定的行为。

Sequence

本节中的技巧针对 Seq 以及它的子类, 而一些转换可以应用于其它集合类,如 Set , Optio n, Map ,甚至 Iterator 类,因为它们提供了相近的接口。

创建Creation

显示创建集合

// Before
Seq[T]()
// After
Seq.empty[T]

有时候可以节省内存(重用empty对象)和CPU (length check浪费)。

也可应用于 Set , Option , Map , Iterator .

Length

对于数组,优先使用 length 而不是 size

// Before
array.size
// After
array.length

lengthsize 基本是同义词。在Scala 2.11中, Array.size 是通过隐式转换实现的。因此每次调用时,一个中间包装类会被创建,除非你允许jvm的 escape analysis 。这会产生多余的GC对象,影响性能。

不要对检查empty的属性取反

// Before
!seq.isEmpty
!seq.nonEmpty
// After
seq.nonEmpty
seq.isEmpty

同样适用于 Set , Option , Map , Iterator

不要通过计算length来检查empty

// Before
seq.length > 0
seq.length != 0
seq.length == 0
// After
seq.nonEmpty
seq.nonEmpty
seq.isEmpty

一方面已经有检查empty的方法,另一方面,,比如 LinearSeq 和子类 List ,会花费 O(n) 的时间计算 length ( IndexedSeq 花费 O(1) )。

同样适用于 Set , Map

不要直接使用 length 来比较

// Before

seq.length > n

seq.length < n

seq.length == n

seq.length != n

// After

seq.lengthCompare(n) > 0

seq.lengthCompare(n) < 0

seq.lengthCompare(n) == 0

seq.lengthCompare(n) != 0

同上一条,计算 length 有时候非常昂贵,有可能将花费从 O(length) 减少到 O(length min n)

对于无限的stream来说,上面的技巧是绝对必要的。

等价 Equality

不要使用 == 比较数组:

// Before

array1 == array2

// After

array1.sameElements(array2)

因为 == 只是比较实例对象,而不是里面的元素。

同样适用于 Iterator

不要检查不同分类(categories)的集合的相等性

// Before

seq == set

// After

seq.toSet == set

不要使用 sameElements 来比较有序集合

// Before

seq1.sameElements(seq2)

// After

seq1 == seq2

不要手工检查相等性

// Before

seq1.corresponds(seq2)(_ == _)

// After

seq1 == seq2

使用内建的方法。

Indexing

不要使用index得到第一个元素

// Before
seq( 0 )
// After
seq.head

不要使用index得到最后一个元素

// Before

seq(seq.length - 1 )

// After

seq.last

不要显式检查index的边界

// Before

if (i < seq.length) Some(seq(i)) else None

// After

seq.lift(i)

不要仿造 headOption

// Before

if (seq.nonEmpty) Some(seq.head) else None

seq.lift( 0 )

// After

seq.headOption

不要仿造 lastOption

// Before

if (seq.nonEmpty) Some(seq.last) else None

seq.lift(seq.length - 1 )

// After

seq.lastOption

小心 indexOflastIndexOf 参数类型

// Before

Seq( 1 , 2 , 3 ).indexOf( "1" ) // compilable

Seq( 1 , 2 , 3 ).lastIndexOf( "2" ) // compilable

// After

Seq( 1 , 2 , 3 ).indexOf( 1 )

Seq( 1 , 2 , 3 ).lastIndexOf( 2 )

不要构造index的Range

// Before

Range( 0 , seq.length)

// After

seq.indices

不要手工使用index来zip集合

// Before

seq.zip(seq.indices)

// After

seq.zipWithIndex

检查元素的存在 Existence

不要用断言equality predicate 来检查存在

// Before

seq.exists(_ == x)

// After

seq.contains(x)

同样应用于 Set , Option , Iterator

小心 contains 参数类型

// Before

Seq( 1 , 2 , 3 ).contains( "1" ) // compilable

// After

Seq( 1 , 2 , 3 ).contains( 1 )

不要用断言inequality predicate 来检查不存在

// Before

seq.forall(_ != x)

// After

!seq.contains(x)

同样应用于 Set , Option , Iterator

不要统计元素的数量来检查存在

// Before

seq.count(p) > 0

seq.count(p) != 0

seq.count(p) == 0

// After

seq.exists(p)

seq.exists(p)

!seq.exists(p)

同样应用于 Set , Map , Iterator

不要借助 filter 来检查存在

// Before

seq.filter(p).nonEmpty

seq.filter(p).isEmpty

// After

seq.exists(p)

!seq.exists(p)

同样应用于 Set , Option , Map , Iterator

Filtering

不要对断言取反

// Before

seq.filter(!p)

// After

seq.filterNot(p)

同样应用于 Set , Option , Map , Iterator

不要借助 filter 统计元素数量

// Before

seq.filter(p).length

// After

seq.count(p)

调用 filter 会产生一个临时集合,影响GC和性能。

同样应用于 Set , Option , Map , Iterator

不要借助 filter 找到元素的第一个值

// Before

seq.filter(p).headOption

// After

seq.find(p)

同样应用于 Set , Option , Map , Iterator

Sorting

不要手工按一个属性排序

// Before

seq.sortWith(_.property < _.property)

// After

seq.sortBy(_.property)

不要手工按照identity排序

// Before

seq.sortBy(it => it)

seq.sortBy(identity)

seq.sortWith(_ < _)

// After

seq.sorted

一步完成排序反转

// Before

seq.sorted.reverse

seq.sortBy(_.property).reverse

seq.sortWith(f(_, _)).reverse

// After

seq.sorted(Ordering[T].reverse)

seq.sortBy(_.property)(Ordering[T].reverse)

seq.sortWith(!f(_, _))

Reduction

不要手工计算sum

// Before

seq.reduce(_ + _)

seq.fold(z)(_ + _)

// After

seq.sum

seq.sum + z

其它可能用的方法 reduceLeft , reduceRight , foldLeft , foldRight

同样应用于 Set , Iterator

不要手工计算product

// Before

seq.reduce(_ * _)

seq.fold(z)(_ * _)

// After

seq.product

seq.product * z

同样应用于 Set , Iterator

不要手工搜索最小值和最大值

// Before

seq.reduce(_ min _)

seq.fold(z)(_ min _)

// After

seq.min

z min seq.min

// Before

seq.reduce(_ max _)

seq.fold(z)(_ max _)

// After

seq.max

z max seq.max

同样应用于 Set , Iterator

不要仿造 forall

// Before

seq.foldLeft( true )((x, y) => x && p(y))

!seq.map(p).contains( false )

// After

seq.forall(p)

同样应用于 Set , Option (for the second line), Iterator

不要仿造 exists

// Before

seq.foldLeft( false )((x, y) => x || p(y))

seq.map(p).contains( true )

// After

seq.exists(p)

Rewriting

合并连续的 filter 调用

// Before

seq.filter(p1).filter(p2)

// After

seq.filter(x => p1(x) && p2(x))

seq.view.filter(p1).filter(p2).force

同样应用于 Set , Option , Map , Iterator

合并连续的 map 调用

// Before

seq.map(f).map(g)

// After

seq.map(f.andThen(g))

seq.view.map(f).map(g).force

同样应用于 Set , Option , Map , Iterator

filter 完后再排序

// Before

seq.sorted.filter(p)

// After

seq.filter(p).sorted

在调用 map 前不要显式调用反转 reverse

// Before

seq.reverse.map(f)

// After

seq.reverseMap(f)

不要显示反转得到迭代器

// Before

seq.reverse.iterator

// After

seq.reverseIterator

不要通过将集合转成 Set 得到不重复集合

// Before

seq.toSet.toSeq

// After

seq.distinct

不要仿造 slice

// Before

seq.drop(x).take(y)

// After

seq.slice(x, x + y)

同样应用于 Set , Map , Iterator

不要仿造 splitAt

// Before

val seq1 = seq.take(n)

val seq2 = seq.drop(n)

// After

val (seq1, seq2) = seq.spiltAt(n)

不要仿造 span

// Before

val seq1 = seq.takeWhile(p)

val seq2 = seq.dropWhile(p)

// After

val (seq1, seq2) = seq.span(p)

不要仿造 partition

// Before

val seq1 = seq.filter(p)

val seq2 = seq.filterNot(p)

// After

val (seq1, seq2) = seq.partition(p)

不要仿造 takeRight

// Before

seq.reverse.take(n).reverse

// After

seq.takeRight(n)

不要仿造 flatten

// Before (seq: Seq[Seq[T]])

seq.flatMap(it => it)

seq.flatMap(identity)

// After

seq.flatten

同样应用于 Set , Map , Iterator

不要仿造 flatMap

// Before (f: A => Seq[B])

seq.map(f).flatten

// After

seq.flatMap(f)

同样应用于 Set , Option , Iterator

不需要结果时不要用 map

// Before

seq.map(...) // the result is ignored

// After

seq.foreach(...)

同样应用于 Set , Option , Map , Iterator

不要产生临时集合

1.使用view

// Before

seq.map(f).flatMap(g).filter(p).reduce(...)

// After

seq.view.map(f).flatMap(g).filter(p).reduce(...)

  1. 将view转换成一个同样类型的集合

// Before

seq.map(f).flatMap(g).filter(p)

// After

seq.view.map(f).flatMap(g).filter(p).force

如果中间的转换是filter,还可以

seq.withFilter( p ). map ( f )

  1. 将view转换成另一种集合

// Before

seq.map(f).flatMap(g).filter(p).toList

// After

seq.view.map(f).flatMap(g).filter(p).toList

还有一种 “transformation + conversion” 方法:

seq.map(f)(collection.breakOut): List[T]

使用赋值操作符

// Before

seq = seq :+ x

seq = x +: seq

seq1 = seq1 ++ seq2

seq1 = seq2 ++ seq1

// After

seq :+= x

seq +:= x

seq1 ++= seq2

seq1 ++:= seq2

Scala有一个语法糖,自动将 x <op>= y 转换成 x = x <op> y . 如果 op: 结尾,则被认为是右结合的操作符。

一些list和stream的语法:

// Before

list = x :: list

list1 = list2 ::: list

stream = x #:: list

stream1 = stream2 #::: stream

// After

list ::= x

list1 :::= list2

stream #::= x

stream1 #:::= stream2

同样应用于 Set , Map , Iterator

Set

大部分的 Seq 的技巧也可以应用于 Set 。另外还有一些只针对 Set 的技巧。

不要使用 sameElements 比较未排序的集合

// Before

set1.sameElements(set2)

// After

set1 == set2

同样应用于 Map

不要手工计算交集

// Before

set1.filter(set2.contains)

set1.filter(set2)

// After

set1.intersect(set2) // or set1 & set2

不要手工计算diff

// Before

set1.filterNot(set2.contains)

set1.filterNot(set2)

// After

set1.diff(set2) // or set1 &~ set2

Option

Option 并不是集合类,但是它提供了类似的方法和行为。

大部分针对 Seq 的技巧也适用于 Option 。这里列出了一些特殊的只针对 Option 的技巧。

Value

不要使用 NoneOption 比较

// Before

option == None

option != None

// After

option.isEmpty

option.isDefined

不要使用 SomeOption 比较

// Before

option == Some(v)

option != Some(v)

// After

option.contains(v)

!option.contains(v)

不要使用实例类型来检查值的存在性

// Before

option.isInstanceOf[Some[_]]

// After

option.isDefined

不要使用模式匹配来检查值的存在

// Before

option match {

case Some(_) => true

case None => false

}

// After

option.isDefined

同样适用于 Seq , Set

对于检查存在性的属性不要取反

// Before

!option.isEmpty

!option.isDefined

!option.nonEmpty

// After

seq.isDefined

seq.isEmpty

seq.isEmpty

不要检查值的存在性再处理值

// Before

if (option.isDefined) {

val v = option.get

...

}

// After

option.foreach { v =>

...

}

Null

不要通过和 null 比较来构造 Option

// Before

if (v != null ) Some(v) else None

// After

Option(v)

不要显示提供 null 作为备选值

// Before

option.getOrElse( null )

// After

option.orNull

Rewriting

map with getOrElse 转换成 fold

// Before

option.map(f).getOrElse(z)

// After

option.fold(z)(f)

不要仿造 exists

// Before

option.map(p).getOrElse( false )

// After

option.exists(p)

不要手工将 option 转换成 sequence

// Before

option.map(Seq(_)).getOrElse(Seq.empty)

option.getOrElse(Seq.empty) // option: Option[Seq[T]]

// After

option.toSeq

Map

同上,这里只列出针对map的技巧

不要使用 lift 替换 get

// Before
map.lift(n)
// After
map.get(n)

因为没有特别的需要将map值转换成一个Option。

不要分别调用 getgetOrElse

// Before

map.get(k).getOrElse(z)

// After

map.getOrElse(k, z)

不要手工抽取键集合

// Before

map.map(_._1)

map.map(_._1).toSet

map.map(_._1).toIterator

// After

map.keys

map.keySet

map.keysIterator

不要手工抽取值集合

// Before

map.map(_._2)

map.map(_._2).toIterator

// After

map.values

map.valuesIterator

小心使用 filterKeys

// Before

map.filterKeys(p)

// After

map.filter(p(_._1))

因为 filterKeys 包装了原始的集合,并没有复制元素,后续处理得小心。

小心使用 mapValues

// Before

map.mapValues(f)

// After

map.map(f(_._2))

同上。

不要手工filter 键

// Before

map.filterKeys(!seq.contains(_))

// After

map -- seq

使用赋值操作符重新赋值

// Before

map = map + x -> y

map1 = map1 ++ map2

map = map - x

map = map -- seq

// After

map += x -> y

map1 ++= map2

map -= x

map --= seq

补充

除了以上的介绍,建议你看一下官方文档 Scala Collections documentation 。

还有

  • Scala for Project Euler
  • Ninety-nine

最后一段是作者的谦虚话,欢迎提供意见和建议。

正文到此结束
Loading...