将项目迁移到 Scala 2.13 的集合

语言

本文档描述了迁移到 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)
  • 不可变哈希集和哈希映射具有新的实现(ChampHashSetChampHashMap,基于 “CHAMP” 编码)。
  • 新的集合类型
    • immutable.ArraySeq 是一个有效不可变的序列,它包装了一个数组
    • immutable.LazyList 是一个在状态上是惰性的链表,即,它是空的还是非空的。这允许在不计算 head 元素的情况下创建 LazyListimmutable.Stream 已弃用,它具有严格的 head 和惰性的 tail
  • 已删除已弃用的集合(MutableListimmutable.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] 之类的方法的用户以前能够传入 OrderArrayBuffer,但在 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 方法时调用 .toSequnsafeWrapArray)。

我们建议使用 import scala.collection/import scala.collection.immutablecollection.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]

这在迁移期间可能很有用,以捕获对不合格 SeqIndexedSeq 的使用。

破坏性更改有哪些?

下表总结了破坏性更改。“自动迁移规则”列给出了可用于将旧代码自动更新为新预期形式的迁移规则的名称。

说明 旧代码 新代码 自动迁移规则
方法 to[C[_]] 已被删除(它可能会被重新引入,但已弃用) xs.to[List] xs.to(List) Collection213UpgradeCollections213CrossCompat
mapValuesfilterKeys 现在返回 MapView 而不是 Map kvs.mapValues(f) kvs.mapValues(f).toMap RoughlyMapValues
Iterable 不再有 sameElements 操作 xs1.sameElements(xs2) xs1.iterator.sameElements(xs2) Collection213UpgradeCollections213CrossCompat
collection.breakOut 不再存在 val xs: List[Int] = ys.map(f)(collection.breakOut) val xs = ys.iterator.map(f).to(List) Collection213Upgrade
zipMap[K, V] 上现在返回 Iterable map.zip(iterable) map.zip(iterable).toMap Collection213Experimental
ArrayBuilder.make 不再接受括号 ArrayBuilder.make[Int]() ArrayBuilder.make[Int] Collection213UpgradeCollections213CrossCompat

某些类已被移除、设为私有或在新设计中没有等效项

  • ArrayStack
  • mutable.FlatHashTable
  • mutable.HashTable
  • History
  • Immutable
  • IndexedSeqOptimized
  • LazyBuilder
  • mutable.LinearSeq
  • LinkedEntry
  • MapBuilder
  • Mutable
  • MutableList
  • Publisher
  • ResizableArray
  • RevertibleHistory
  • SeqForwarder
  • SetBuilder
  • Sizing
  • SliceInterval
  • StackBuilder
  • StreamView
  • Subscriber
  • Undoable
  • WrappedArrayBuilder

其他值得注意的更改包括

  • Iterable.partition 在非严格集合上两次调用 iterator,并假设它获取了相同元素上的两个迭代器。严格子类重写 partition 仅执行一次遍历
  • 集合之间的相等不再在 Iterable 级别定义。它在 SetSeqMap 分支中分别定义。另一个结果是 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 Collection213UpgradeCollections213CrossCompat
mutable.Map 不再有 updated 方法 mutable.Map(1 -> 2).updated(1, 3) mutable.Map(1 -> 2).clone() += 1 -> 3 Collection213UpgradeCollections213CrossCompat
mutable.Set/Map 不再有 + 操作 mutable.Set(1) + 2 mutable.Set(1).clone() += 2 Collection213UpgradeCollections213CrossCompat
SortedSettountilfrom 方法现在分别称为 rangeTorangeUntilrangeFrom xs.until(42) xs.rangeUntil(42)  
TraversableTraversableOnce 分别替换为 IterableIterableOnce def f(xs: Traversable[Int]): Unit def f(xs: Iterable[Int]): Unit Collection213UpgradeCollections213CrossCompat
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) Collection213UpgradeCollections213CrossCompat
IterableOnce#toIterator 替换为 IterableOnce#iterator xs.toIterator xs.iterator Collection213UpgradeCollections213CrossCompat
copyToBuffer 已弃用 xs.copyToBuffer(buffer) buffer ++= xs Collection213UpgradeCollections213CrossCompat
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) Collection213UpgradeCollections213CrossCompat
companion 操作已重命名为 iterableFactory xs.companion xs.iterableFactory  

2.12 中已弃用但在 2.13 中已删除的内容

  • collection.JavaConversions。改为使用 scala.jdk.CollectionConverters。之前的建议是使用 collection.JavaConverters,该建议现已弃用 ;
  • collection.mutable.MutableList(在 2.12 中未弃用,但被认为是实现其他集合的实现细节)。改为使用 ArrayDequemutable.ListBuffer,或 Listvar ;
  • collection.immutable.Stack。改为使用 List ;
  • StackProxyMapProxySetProxySeqProxy 等。无替换 ;
  • SynchronizedMapSynchronizedBuffer 等,改用 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

其他新操作有 distinctBypartitionMap

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._

提供了以下操作

  • Seq
    • intersperse
  • Map
    • zipByKey / join / zipByKeyWith
    • mergeByKey / fullOuterJoin / mergeByKeyWith / leftOuterJoin / rightOuterJoin

现有集合类型的新实现(性能特征的变化)?

默认 SetMap 分别由 ChampHashSetChampHashMap 支持。性能特征相同,但操作实现更快。这些数据结构的内存占用也更低。

mutable.Queuemutable.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

集合实现者

要了解在实现自定义集合类型或操作时存在的差异,请参阅以下文档

此页面的贡献者