1. 序章

時間の経過とともに、Scalaコレクションは、コードの再利用を最大化するように進化し、それらすべてで機能する一貫性のある共通のAPIを目標としています。 したがって、それらは、プログラマーが実際の実装を抽象化するのに役立ついくつかの演算子を共有します。

ただし、 List は、元の演算子とメソッドも保持しています。 したがって、いくつかの操作をさまざまな方法で実装できます。 そのような操作の1つが連結です。

このチュートリアルでは、:::演算子と++演算子の相違点と類似点を確認することにより、Listの連結に焦点を当てます。

2. リスト連結

このセクションでは、Listオブジェクトを連結するために使用される2つの演算子:::++について説明します。 それぞれのオペレーターの動作を確認してから、2つを比較します。

2.1. :::演算子

:::演算子はリスト固有です。 クラスList[A]がそれをどのように定義するかを見てみましょう。

def :::[B >: A](prefix: List[B]): List[B]

オペレーターはパラメーターとしてList[B] を取ります。ここで、BAのスーパータイプでなければなりません。 次に、返されるListの要素の静的タイプもBになります。

::: は、基本的に::演算子の右結合バージョンです。 したがって、 List(1、2)::: List(3、 4)と書くことは、 List(3、4)。:::(List(1、 2)と同等です。 ))。 したがって、:::prependメソッドと同等です。

この演算子は、最初のリストの最後の要素が2番目のの先頭を指す新しいListオブジェクトを作成します。

val list1 = 1 :: 2 :: 3 :: Nil
val list2 = 4 :: 5 :: Nil

assert(list1 ::: list2 == 1 :: 2 :: 3 :: 4 :: 5 :: Nil)

ここで、list1list2は、それぞれ 1 :: 2 :: 3 ::Nil4:: 5 ::Nilです。 ]。 ::: を適用すると、結果は 1 :: 2 :: 3 :: 4 :: 5 ::Nilになります。 list1Nillist2の先頭へのポインターに置き換えられていることに注目してください。

さらに、 ::: を使用して、3つ以上のリストを連続して連結することもできます。

val list1 = 1 :: 2 :: 3 :: Nil
val list2 = 4 :: 5 :: Nil
val list3 = 6 :: 7 :: Nil

assert(list1 ::: list2 ::: list3 == 1 :: 2 :: 3 :: 4 :: 5 :: 6 :: 7 :: Nil)

2.2. ++演算子

++ 演算子の定義は、:::演算子の定義よりも複雑です。 List[A]++演算子をどのように定義するかを見てみましょう。

override def ++[B >: A, That](that: GenTraversableOnce[B])(implicit bf: CanBuildFrom[List[A], B, That]): That

++ 演算子は、タイプBに多くの追加の制約を課します。 A のスーパータイプであることに加えて、 ++ では、そのパラメーターがGenTraversableOnceのインスタンスである必要があります。 つまり、 That は、 List Set Map 、またはStringである可能性があります。 実際のところ、 GenTraversableOnceは、すべてのtraversable-onceオブジェクトの親特性です。

さらに、 ++ では、スコープで使用できるように、タイプ CanBuildFrom [List [A]、B、That]implicit値が必要です。 つまり、コンパイラは、 List [A] (リストから、実際の値がまだ推測されていないタイプThatのコレクションを構築する方法を見つけることができるはずです。 ++ が呼び出されています)および B (連結するコレクション内の要素のタイプ)。

Scalaは、スコープで見つけた最も適切な暗黙の値に基づいて、実際のThatタイプを推測します。 そのような値が見つからない場合(つまり、Scalaが結果のコレクションを構築する方法を知らない場合)、コンパイルは失敗します。

実際にそれを使用する方法を見てみましょう:

val list1 = 1 :: 2 :: 3 :: Nil
val list2 = 4 :: 5 :: Nil

assert(list1 ++ list2 == 1 :: 2 :: 3 :: 4 :: 5 :: Nil)

結果は以前と同じです。 さらに、複数のリストを1行に連結することもできます。

val list1 = 1 :: 2 :: 3 :: Nil
val list2 = 4 :: 5 :: Nil
val list3 = 6 :: 7 :: Nil

assert(list1 ++ list2 ++ list3 == 1 :: 2 :: 3 :: 4 :: 5 :: 6 :: 7 :: Nil)

2.3. :::++の違い

上で見たように、:::または++のいずれかを使用しても同じ結果が得られます。 ただし、2つの演算子の間には2つの主な違いがあります。

最初の違いはパフォーマンスです。 ::: は右結合であるため(Scalaではで始まるすべての演算子と同様)、Scalaは list1 ::: list2 :::list3を解析します。 ] as list1 :::(list2 ::: list3)。 一方、Scalaは list1 ++ list2 ++ list3 を( list1 ++ list2)++list3として解析します。

Scalaコレクションは不変であるため、ランタイムはリスト全体を反復処理して別のリストを追加する必要があります。 list1 :::(list2 ::: list3)list2を繰り返してlist3を追加し、次にlist1を繰り返して[の結果を追加しますX143X] list2 :::list3。 逆に、( list1 ++ list2)++ list3 は、list1を繰り返してlist2を追加し、 list1 ++list2[の結果を追加します。 を追加してlist3を追加します。

後者の場合、 list1 は2回繰り返されますが、前者の場合は1回だけ繰り返されます。 したがって、パフォーマンスの観点から、:::演算子は++よりも効率的です。 これは、2つのリストだけを連結する場合には当てはまりませんが、2つ以上のリストを続けて連結すると、ますます明らかになります。

2番目の違いは、型安全性についてです。 前に見たように、:::Listでのみ機能しますが、++はすべてのGenTraversableOnceで機能します。 実際のところ、 ++ を使用すると、コレクションが同じタイプでなくても、2つのコレクションの和集合を作成できます。

assert(List(1, 2) ++ Map(3 -> 4) == List(1, 2, 3 -> 4))
assert(List(1, 2, 3) ++ "ab" == List(1, 2, 3, 'a', 'b'))

List(1、2)++ Map(3-> 4)は、 List [Any] を返します。これは、2つのコレクションから他の一般的なスーパータイプを推測できないためです。 同様に、 List[Int]Stringを連結すると、 List[Any]が再び生成されます。

一方、上記の2つの例で ::: を使用すると、コンパイルエラーが発生します。

3. 結論

この記事では、:::++の相違点と類似点を見て、Scalaでのリストの連結に焦点を当てました。

いつものように、完全なソースコードはGitHubにあります。