1. 概要

インターフェイスの誤用を防ぐために、内部状態を可変コレクション内に保持したいが、不変バリアントを公開したい場合があります。 この記事では、可変コレクションを不変コレクションに変更するさまざまな方法を見ていきます。

2. 組み込みの不変コレクション

アプローチの1つは、Kotlinのデフォルトの可変および不変コレクションを使用することです。 例として、MutableListListを見てみましょう。 まず、可変リストを作成し、それに要素を追加します。

val mutableList = mutableListOf<String>()

mutableList.add("Hello")

// Prints "Hello"
println(mutableList.joinToString())

可変リストから不変リストを作成できます。

val immutableList: List<String> = mutableList.toList()

ただし、変更しようとするとエラーが発生します。

// Throws an error
immutableList[0] = "World"

不変リストの可変コピーを作成して、そのコピーを変更することもできます。

val backToMutableList = immutableList.toMutableList()

backToMutableList[0] = "World"

// Prints "World"
println(backToMutableList.joinToString())

// Prints "Hello"
println(mutableList.joinToString())

デフォルトの方法で日常的に使用できますが、完全なコピーを作成できない場合があります。たとえば、パフォーマンスが重要な場合などです。 このような場合、不変型にキャストしてみることができます。 ここでの問題は、特に頑固なユーザーが基になるデータを変更できるようになる可能性があることです。

toList を使用する代わりに、前に示した mutableList を使用して、単純にキャストしてみましょう。

// Just casting
val immutableList: List<String> = mutableList

繰り返しますが、キャスト先のタイプは必要な操作を公開していないため、変更できません。

// Throws an error
immutableList[0] = "World"

しかし、ここで頑固なユーザーが登場します。 単純なキャストのみを行ったため、ユーザーはそれをMutableListにキャストし直すことができます。

// Unsafe casting
val backToMutableList = immutableList as MutableList<String>

次に、元のコレクションを変更します。

backToMutableList[0] = "World"

// Prints "World"
println(backToMutableList.joinToString())
 
// Prints "World"
println(mutableList.joinToString())

ご覧のとおり、「キャストだけ」のアプローチは深刻な誤用につながる可能性があります。 しかし、何らかの理由で、組み込みのメソッドを使用してコレクションをコピーしたくない場合、どのようなオプションがありますか? 組み込みの収集方法では不十分な場合はどうすればよいですか?

3. ビルトインが十分でない場合:委任アプローチ

私たちにできることの1つは、必要なコレクションを別のタイプにラップすることです。 Kotlinの委任機能のおかげで、これは簡単に実装できます。 まず、使用するコレクションの不変バージョンを定義します。

class ImmutableList<T>(private val protectedList: List<T>): List<T> by protectedList

このようなラッパーができたら、それを使用して前の例で発生した問題を修正できます。 もう一度、mutableListを使用しましょう。

// Prints "Hello"
println(mutableList.joinToString())

ただし、 toList を使用したりキャストしたりする代わりに、ImmutableListでラップしましょう。

// Wrap - no copy!
val immutableList = ImmutableList(mutableList)

以前と同様に、追加のメソッドにアクセスできず、ラッピングのためにimmutableListMutableListにキャストできません。

// Error - Immutable List does not have addition methods
immutableList[0] = "World"

// Error - Cannot cast to Mutable list
val backToMutable = immutableList as MutableList<String>

これで、問題が解決しました。ラッパーを使用して、Kotlinの toList の機能をコピーせずに、インターフェイスユーザーがコレクションをMutableListにキャストする可能性を無効にできます。 さらに良いのは、このようなラッパーを自分で実装する必要がまったくないことです。 誰かがすでに私たちのためにそれをしました。

3.1. Klutterの実装

人気のあるKotlinユーティリティライブラリKlutterは、不変コレクションの実装をすでに提供しています。 これは、前述の委任アプローチを使用しますが、より機能が完全な方法です。

このソリューションを使用するために、Klutterを依存関係としてプロジェクトに追加しましょう。

<dependency>
    <groupId>uy.kohesive.klutter</groupId>
    <artifactId>klutter-core</artifactId>
    <version>3.0.0</version>
</dependency>

それが済んだら、Klutterを使用してあらゆる種類の不変のコレクションを取得できます。 マップまたはセットがあるとしましょう。

val map = mutableMapOf<String, String>()
val set = mutableSetOf<String>()

提供されている拡張機能を使用して、たとえば、コレクションをコピーしてデリゲートにラップできます。

map.toImmutable()

または、コレクションをデリゲートでラップします。

set.asReadOnly()

Klutterがこのアプローチを実装する方法にはまだまだありますが、それはまったく別の記事である可能性があります。 今のところ、Klutterのソースコードを直接見ることができます。

4. 結論

この記事では、Kotlinで不変のコレクションを実現するための複数の方法を学びました。 いつものように、完全なコードはGitHubから入手できます。