將專案遷移至 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 變數引數方法的類型。
  • 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 是在狀態上延遲的連結清單,亦即,它是空的或非空的。這允許建立 LazyList,而無需評估 head 元素。具有嚴格 head 和延遲 tailimmutable.Stream 已不建議使用。
  • 已移除不建議使用的集合 (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、變數長度引數和 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] 的別名。這些變更需要一些規劃,視您的程式碼將如何使用而定。

由於 SLS 6.6scala.Seq 定義的變更也會產生變數長度引數參數的類型為不可變序列的效果,因此在 orderFood(xs: _*) 等方法中,變數長度引數參數 xs 必須是不可變序列。

因此,Scala 2.13 中包含 scala.Seq、可變長參數或 scala.IndexedSeq 的任何方法簽章都會在 API 語意上有重大變更(因為不可變序列類型需要比不可變類型更多——不可變性)。例如,def orderFood(order: Seq[Order]): Seq[Food] 等方法的使用者先前能夠傳入 OrderArrayBuffer,但在 2.13 中則不行。

變更可變長參數

變更可變長參數是不可避免的,因為您無法變更定義網站中使用的類型。可供變更使用網站的選項如下

  • 將值變更為不可變序列,允許直接使用可變長參數:xs: _*,
  • 透過呼叫 .toSeq 將值變更為不可變序列:xs.toSeq: _*,這僅會在序列尚未不可變時複製資料
  • 使用 scala.collection.immutable.ArraySeq.unsafeWrapArray 包裝陣列並避免複製,但請參閱其 scaladoc

選項 1:變更回 scala.collection.Seq

對於 scala.Seq 的所有非可變長參數使用,某種程度上最簡單的變更策略是將其替換為 scala.collection.Seq(並要求使用者在將此類序列傳遞給可變長參數方法時呼叫 .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,遵循 穩健性原則(也稱為「Postel 定律」)

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]
}

遮蔽 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
Map[K, V] 上的 zip 現在會傳回 Iterable map.zip(iterable) map.zip(iterable).toMap Collection213Experimental
ArrayBuilder.make 不再接受括號 ArrayBuilder.make[Int]() ArrayBuilder.make[Int] Collection213UpgradeCollections213CrossCompat

有些類別已移除、設為私有或在新設計中沒有等效類別

  • ArrayStack
  • 可變的 FlatHashTable
  • 可變的 HashTable
  • 歷史
  • 不可變的
  • IndexedSeqOptimized
  • LazyBuilder
  • 可變的 LinearSeq
  • LinkedEntry
  • MapBuilder
  • 可變的
  • 可變的 List
  • Publisher
  • ResizableArray
  • RevertibleHistory
  • SeqForwarder
  • SetBuilder
  • 大小調整
  • SliceInterval
  • StackBuilder
  • StreamView
  • Subscriber
  • 可復原的
  • 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
StreamLazyList 取代 Stream.from(1) LazyList.from(1) Collection213Roughly
Seq#unionconcat 取代 xs.union(ys) xs.concat(ys)  
Stream#appendlazyAppendAll 取代 xs.append(ys) xs.lazyAppendedAll(ys) Collection213UpgradeCollections213CrossCompat
IterableOnce#toIteratorIterableOnce#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 是由陣列支援的不可變序列。它用於傳遞 varargs 參數。

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

集合實作

若要瞭解實作自訂集合類型或運算時的不同之處,請參閱下列文件

此頁面的貢獻者