本文档描述了迁移到 Scala 2.13 的集合用户的更改,并展示了如何使用 Scala 2.11/2.12 和 2.13 跨构建项目。
有关 Scala 2.13 集合库的深入概述,请参阅 集合指南。2.13 集合的实现细节在文档 Scala 集合的架构 中进行了解释。
Scala 2.13 集合库中最重要的更改是
scala.Seq[+A]现在是scala.collection.immutable.Seq[A]的别名(而不是scala.collection.Seq[A])。请注意,这也更改了 Scala varargs 方法的类型。scala.IndexedSeq[+A]现在是scala.collection.immutable.IndexedSeq[A]的别名(而不是scala.collection.IndexedSeq[A])。- 转换方法不再具有隐式
CanBuildFrom参数。这使得库更容易理解(在源代码、Scaladoc 和 IDE 代码补全中)。它还使编译用户代码更有效率。 - 类型层次结构已简化。
Traversable不再存在,只有Iterable。 to[Collection]方法已被to(Collection)方法替换。- 按照惯例,
toC方法是严格的,并在适用时生成默认的集合类型。例如,Iterator.continually(42).take(10).toSeq会生成一个List[Int],而如果没有限制则不会生成。 - 无论在何处定义,
toIterable都已弃用。特别是对于Iterator,优先使用to(LazyList)。 - 视图已大大简化,现在工作可靠。它们不再扩展其对应的集合类型,例如,
IndexedSeqView不再扩展IndexedSeq。 collection.breakOut不再存在,请改用.view和.to(Collection)。- 不可变哈希集和哈希映射具有新的实现(
ChampHashSet和ChampHashMap,基于 “CHAMP” 编码)。 - 新的集合类型
immutable.ArraySeq是一个有效不可变的序列,它包装了一个数组immutable.LazyList是一个在状态上是惰性的链表,即,它是空的还是非空的。这允许在不计算head元素的情况下创建LazyList。immutable.Stream已弃用,它具有严格的head和惰性的tail。
- 已删除已弃用的集合(
MutableList、immutable.Stack等) - 并行集合现在位于一个单独的层次结构中,在 单独的模块 中。
scala.jdk.StreamConverters对象提供了扩展方法,用于为 Scala 集合创建(顺序或并行)Java 8 流。
用于迁移和交叉构建的工具
scala-collection-compat 是为 2.11、2.12 和 2.13 发布的一个库,它为旧版本提供了 Scala 2.13 中的一些新 API。这简化了交叉构建项目。
该模块还为 scalafix 提供了 迁移规则,可以更新项目的源代码以使用 2.13 集合库。
scala.Seq、varargs 和 scala.IndexedSeq 迁移
在 Scala 2.13 中,scala.Seq[+A] 是 scala.collection.immutable.Seq[A] 的别名,而不是 scala.collection.Seq[A],而 scala.IndexedSeq[+A] 是 scala.collection.immutable.IndexedSeq[A] 的别名。这些更改需要根据代码的使用方式进行一些规划。
scala.Seq 定义的更改还导致 varargs 参数的类型变为不可变序列,这是由于 SLS 6.6,因此在诸如 orderFood(xs: _*) 的方法中,varargs 参数 xs 必须是不可变序列。
因此,Scala 2.13 中包含 scala.Seq、varargs 或 scala.IndexedSeq 的任何方法签名都将在 API 语义中发生重大更改(因为不可变序列类型需要比不可变类型更多——不可变性)。例如,诸如 def orderFood(order: Seq[Order]): Seq[Food] 之类的方法的用户以前能够传入 Order 的 ArrayBuffer,但在 2.13 中不能这样做。
迁移 varargs
varargs 的更改是不可避免的,因为您无法更改定义站点中使用的类型。可用于迁移使用站点的选项如下
- 将值更改为已经是一个不可变序列,这允许直接使用 varargs:
xs: _*, - 通过调用
.toSeq:xs.toSeq: _*,可以将值更改为不可变序列,这仅在序列不是不可变时才复制数据 - 使用
scala.collection.immutable.ArraySeq.unsafeWrapArray包装数组并避免复制,但请参阅其 scaladoc
选项 1:迁移回 scala.collection.Seq
对于所有非 varargs 用法,第一个也是某种程度上最简单的 scala.Seq 迁移策略是将它们替换为 scala.collection.Seq(并要求用户在将此类序列传递给 varargs 方法时调用 .toSeq 或 unsafeWrapArray)。
我们建议使用 import scala.collection/import scala.collection.immutable 和 collection.Seq/immutable.Seq。
我们建议不要使用 import scala.collection.Seq,因为它会隐藏自动导入的 scala.Seq,即使它是一行更改,也会导致名称混淆。对于代码生成或宏,最安全的选择是使用完全限定的 _root_.scala.collection.Seq。
例如,迁移看起来像这样
import scala.collection
object FoodToGo {
def orderFood(order: collection.Seq[Order]): collection.Seq[Food]
}
但是,Scala 2.13 中此代码的用户也必须迁移,因为结果类型与任何 scala.Seq(或仅 Seq)在其代码中的用法不兼容
val food: Seq[Food] = FoodToGo.orderFood(order) // won't compile
最简单的解决方法是要求用户对结果调用 .toSeq,这将返回一个不可变 Seq,并且仅在序列不是不可变时才复制数据
val food: Seq[Food] = FoodToGo.orderFood(order).toSeq // add .toSeq
选项 2:对参数使用 scala.collection.Seq,对结果类型使用 scala.collection.immutable.Seq
第二个中间迁移策略是更改所有方法以接受不可变 Seq 但返回不可变 Seq,遵循 鲁棒性原则(也称为“波斯特法则”)
import scala.collection
import scala.collection.immutable
object FoodToGo {
def orderFood(order: collection.Seq[Order]): immutable.Seq[Food]
}
选项 3:使用不可变序列
第三个迁移策略是更改 API 以对参数和结果类型都使用不可变序列。在为 Scala 2.12 和 2.13 交叉构建库时,这可能意味着
- 继续使用
scala.Seq,这意味着它在 2.12 中保持源和二进制兼容,但必须具有不可变序列语义(但这可能已经是这种情况)。 - 在 Scala 2.12 和 2.13 中明确使用不可变 Seq,这意味着在 2.12 中破坏源、二进制和(可能)语义兼容性
import scala.collection.immutable
object FoodToGo {
def orderFood(order: immutable.Seq[Order]): immutable.Seq[Food]
}
Shadowing scala.Seq 和 scala.IndexedSeq
您可能对完全禁止使用普通 Seq 感兴趣。您可以通过声明自己的包级(和包私有)Seq 类型来使用编译器来执行此操作,该类型将屏蔽 scala.Seq。
package example
import scala.annotation.compileTimeOnly
/**
* In Scala 2.13, `scala.Seq` changed from aliasing `scala.collection.Seq` to aliasing
* `scala.collection.immutable.Seq`. In this code base usage of unqualified `Seq` is banned: use
* `immutable.Seq` or `collection.Seq` instead.
*
* import scala.collection
* import scala.collection.immutable
*
* This `Seq` trait is a dummy type to prevent the use of `Seq`.
*/
@compileTimeOnly("Use immutable.Seq or collection.Seq")
private[example] trait Seq[A1]
/**
* In Scala 2.13, `scala.IndexedSeq` changed from aliasing `scala.collection.IndexedSeq` to aliasing
* `scala.collection.immutable.IndexedSeq`. In this code base usage of unqualified `IndexedSeq` is
* banned: use `immutable.IndexedSeq` or `collection.IndexedSeq`.
*
* import scala.collection
* import scala.collection.immutable
*
* This `IndexedSeq` trait is a dummy type to prevent the use of `IndexedSeq`.
*/
@compileTimeOnly("Use immutable.IndexedSeq or collection.IndexedSeq")
private[example] trait IndexedSeq[A1]
这在迁移期间可能很有用,以捕获对不合格 Seq 和 IndexedSeq 的使用。
破坏性更改有哪些?
下表总结了破坏性更改。“自动迁移规则”列给出了可用于将旧代码自动更新为新预期形式的迁移规则的名称。
| 说明 | 旧代码 | 新代码 | 自动迁移规则 |
|---|---|---|---|
方法 to[C[_]] 已被删除(它可能会被重新引入,但已弃用) |
xs.to[List] |
xs.to(List) |
Collection213Upgrade,Collections213CrossCompat |
mapValues 和 filterKeys 现在返回 MapView 而不是 Map |
kvs.mapValues(f) |
kvs.mapValues(f).toMap |
RoughlyMapValues |
Iterable 不再有 sameElements 操作 |
xs1.sameElements(xs2) |
xs1.iterator.sameElements(xs2) |
Collection213Upgrade,Collections213CrossCompat |
collection.breakOut 不再存在 |
val xs: List[Int] = ys.map(f)(collection.breakOut) |
val xs = ys.iterator.map(f).to(List) |
Collection213Upgrade |
zip 在 Map[K, V] 上现在返回 Iterable |
map.zip(iterable) |
map.zip(iterable).toMap |
Collection213Experimental |
ArrayBuilder.make 不再接受括号 |
ArrayBuilder.make[Int]() |
ArrayBuilder.make[Int] |
Collection213Upgrade,Collections213CrossCompat |
某些类已被移除、设为私有或在新设计中没有等效项
ArrayStackmutable.FlatHashTablemutable.HashTableHistoryImmutableIndexedSeqOptimizedLazyBuildermutable.LinearSeqLinkedEntryMapBuilderMutableMutableListPublisherResizableArrayRevertibleHistorySeqForwarderSetBuilderSizingSliceIntervalStackBuilderStreamViewSubscriberUndoableWrappedArrayBuilder
其他值得注意的更改包括
Iterable.partition在非严格集合上两次调用iterator,并假设它获取了相同元素上的两个迭代器。严格子类重写partition仅执行一次遍历- 集合之间的相等不再在
Iterable级别定义。它在Set、Seq和Map分支中分别定义。另一个结果是Iterable不再具有canEqual方法。 -
新集合更多地利用了重载。您可以在 此处 找到有关此选择背后的动机的更多信息。例如,
Map.map已重载scala> Map(1 -> "a").map def map[B](f: ((Int, String)) => B): scala.collection.immutable.Iterable[B] def map[K2, V2](f: ((Int, String)) => (K2, V2)): scala.collection.immutable.Map[K2,V2]类型推断已得到改进,因此
Map(1 -> "a").map(x => (x._1 + 1, x._2))可行,编译器可以推断函数文字的参数类型。但是,在 2.13.0-M4 中使用方法引用(2.13.0 的改进正在进行中)不可行,需要显式 eta 展开scala> def f(t: (Int, String)) = (t._1 + 1, t._2) scala> Map(1 -> "a").map(f) ^ error: missing argument list for method f Unapplied methods are only converted to functions when a function type is expected. You can make this conversion explicit by writing `f _` or `f(_)` instead of `f`. scala> Map(1 -> "a").map(f _) res10: scala.collection.immutable.Map[Int,String] = ChampHashMap(2 -> a) View已完全重新设计,我们预计其用法将具有更可预测的评估模型。您可以在 此处 了解有关新设计的更多信息。mutable.ArraySeq(在 2.12 中封装Array[AnyRef],这意味着基本类型在数组中被装箱)现在可以封装装箱和未装箱数组。2.13 中的mutable.ArraySeq实际上等同于 2.12 中的WrappedArray,基本类型数组有专门的子类。请注意,mutable.ArraySeq可用于基本类型数组(TODO:记录使用方法)。WrappedArray已弃用。- 没有“默认”
Factory(以前称为[A, C] => CanBuildFrom[Nothing, A, C]):请明确使用Factory[A, Vector[A]]。 Array.deep已删除。
仍支持旧语法,但会发生重大更改
下表列出了继续使用弃用警告正常工作的更改。
| 说明 | 旧代码 | 新代码 | 自动迁移规则 |
|---|---|---|---|
collection.Set/Map 不再有 + 和 - 操作 |
xs + 1 - 2 |
xs ++ Set(1) -- Set(2) |
Collection213Experimental |
collection.Map 不再有 -- 操作 |
map -- keys |
map.to(immutable.Map) -- keys |
|
immutable.Set/Map:+ 操作不再具有接受多个值的高负荷 |
Set(1) + (2, 3) |
Set(1) + 2 + 3 |
Collection213Upgrade,Collections213CrossCompat |
mutable.Map 不再有 updated 方法 |
mutable.Map(1 -> 2).updated(1, 3) |
mutable.Map(1 -> 2).clone() += 1 -> 3 |
Collection213Upgrade,Collections213CrossCompat |
mutable.Set/Map 不再有 + 操作 |
mutable.Set(1) + 2 |
mutable.Set(1).clone() += 2 |
Collection213Upgrade,Collections213CrossCompat |
SortedSet:to、until 和 from 方法现在分别称为 rangeTo、rangeUntil 和 rangeFrom |
xs.until(42) |
xs.rangeUntil(42) |
|
Traversable 和 TraversableOnce 分别替换为 Iterable 和 IterableOnce |
def f(xs: Traversable[Int]): Unit |
def f(xs: Iterable[Int]): Unit |
Collection213Upgrade,Collections213CrossCompat |
Stream 替换为 LazyList |
Stream.from(1) |
LazyList.from(1) |
Collection213Roughly |
Seq#union 替换为 concat |
xs.union(ys) |
xs.concat(ys) |
|
Stream#append 替换为 lazyAppendAll |
xs.append(ys) |
xs.lazyAppendedAll(ys) |
Collection213Upgrade,Collections213CrossCompat |
IterableOnce#toIterator 替换为 IterableOnce#iterator |
xs.toIterator |
xs.iterator |
Collection213Upgrade,Collections213CrossCompat |
copyToBuffer 已弃用 |
xs.copyToBuffer(buffer) |
buffer ++= xs |
Collection213Upgrade,Collections213CrossCompat |
TupleNZipped 已替换为 LazyZipN |
(xs, ys).zipped |
xs.lazyZip(ys) |
Collection213Upgrade |
retain 已重命名为 filterInPlace |
xs.retain(f) |
xs.filterInPlace(f.tupled) |
Collection213Upgrade |
:/ 和 /: 运算符已弃用 |
(xs :\ y)(f) |
xs.foldRight(y)(f) |
Collection213Upgrade,Collections213CrossCompat |
companion 操作已重命名为 iterableFactory |
xs.companion |
xs.iterableFactory |
2.12 中已弃用但在 2.13 中已删除的内容
collection.JavaConversions。改为使用scala.jdk.CollectionConverters。之前的建议是使用collection.JavaConverters,该建议现已弃用 ;collection.mutable.MutableList(在 2.12 中未弃用,但被认为是实现其他集合的实现细节)。改为使用ArrayDeque或mutable.ListBuffer,或List和var;collection.immutable.Stack。改为使用List;StackProxy、MapProxy、SetProxy、SeqProxy等。无替换 ;SynchronizedMap、SynchronizedBuffer等,改用java.util.concurrent;
新增集合类型?
scala.collection.immutable.ArraySeq 是由数组支持的不可变序列。用于传递可变参数。
scala-collection-contrib 模块提供了装饰器,用新操作丰富集合。可以将此工件视为孵化器:如果我们获得证据表明这些操作应成为核心的一部分,我们最终可能会将它们移走。
提供了以下集合
MultiSet(可变和不可变)SortedMultiSet(可变和不可变)MultiDict(可变和不可变)SortedMultiDict(可变和不可变)
新增集合操作?
提供了以下新的分区操作
def groupMap[K, B](key: A => K)(f: A => B): Map[K, CC[B]] // (Where `CC` can be `List`, for instance)
def groupMapReduce[K, B](key: A => K)(f: A => B)(g: (B, B) => B): Map[K, B]
groupMap 等价于 groupBy(key).mapValues(_.map(f))。
groupMapReduce 等价于 groupBy(key).mapValues(_.map(f).reduce(g))。
可变集合现在具有修改集合的转换操作
def mapInPlace(f: A => A): this.type
def flatMapInPlace(f: A => IterableOnce[A]): this.type
def filterInPlace(p: A => Boolean): this.type
def patchInPlace(from: Int, patch: scala.collection.Seq[A], replaced: Int): this.type
其他新操作有 distinctBy 和 partitionMap
def distinctBy[B](f: A => B): C // `C` can be `List[Int]`, for instance
def partitionMap[A1, A2](f: A => Either[A1, A2]): (CC[A1], CC[A2]) // `CC` can be `List`, for instance
最后,scala-collection-contrib 模块提供了其他操作。可以将此工件视为孵化器:如果我们获得证据表明这些操作应成为核心的一部分,我们最终可能会将它们移走。
通过隐式丰富提供了新操作。需要添加以下导入才能使它们可用
import strawman.collection.decorators._
提供了以下操作
Seqintersperse
MapzipByKey/join/zipByKeyWithmergeByKey/fullOuterJoin/mergeByKeyWith/leftOuterJoin/rightOuterJoin
现有集合类型的新实现(性能特征的变化)?
默认 Set 和 Map 分别由 ChampHashSet 和 ChampHashMap 支持。性能特征相同,但操作实现更快。这些数据结构的内存占用也更低。
mutable.Queue 和 mutable.Stack 现在使用 mutable.ArrayDeque。此数据结构支持常量时间索引访问,以及摊销常量时间插入和删除操作。
如何针对 Scala 2.12 和 Scala 2.13 交叉构建我的项目?
大多数集合用法都是兼容的,并且可以交叉编译 2.12 和 2.13(有时会产生一些警告)。
如果您无法交叉编译代码,则有各种解决方案
- 您可以使用
scala-collection-compat库,它使 2.13 的某些 API 可用于 2.11 和 2.12。此解决方案并不总是有效,例如,如果您的库实现了自定义集合类型。 - 您可以维护一个单独的分支,其中包含针对 2.13 的更改,并从此分支发布针对 2.13 的版本。
-
您可以将无法交叉编译的源文件放在单独的目录中,并配置 sbt 根据 Scala 版本组装源(另请参阅以下示例)
// Adds a `src/main/scala-2.13+` source directory for Scala 2.13 and newer // and a `src/main/scala-2.13-` source directory for Scala version older than 2.13 unmanagedSourceDirectories in Compile += { val sourceDir = (sourceDirectory in Compile).value CrossVersion.partialVersion(scalaVersion.value) match { case Some((2, n)) if n >= 13 => sourceDir / "scala-2.13+" case _ => sourceDir / "scala-2.13-" } }
使用单独源目录交叉编译的库示例
- https://github.com/scala/scala-parser-combinators/pull/152
- https://github.com/scala/scala-xml/pull/222
- 此处列出了一些其他示例:https://github.com/scala/community-builds/issues/710
集合实现者
要了解在实现自定义集合类型或操作时存在的差异,请参阅以下文档