1. 概要

リストは、開発者が日常業務で使用する最も一般的なデータ構造の1つです。 リストに多くの要素が含まれている場合は特に、リストを効率的に操作することが非常に必要になる場合があります。

リストをインプレースで変更するということは、リストをループすると同時にその値を更新することを意味します。 これは、リストで実行している操作がリストの前の要素に依存している場合に役立つことがあります。

この記事では、リストを効率的に更新するために使用できるいくつかの方法について説明します。

2. 可変および不変リスト

Kotlinでは、リストは可変または不変のいずれかです。 可変リストを使用すると、要素を追加および変更できます。 一方、不変リストを作成した後は、それを変更することはできません。

インプレース変更を行いたいので、この記事では可変リストを使用します。

3. イテレータを使用してインプレースで変更

イテレータを使用してコレクションを反復処理できます。 MutableList インターフェイスには、 listIterator()メソッドが含まれています。このメソッドはMutableListIteratorクラスを返します。

MutableListIteratorには、リストをループするための一般的なhasNext()メソッドとnext()メソッドがありますが、setメソッドも含まれています。 このメソッドは、 next()メソッドを介して取得された最後の要素の値を変更します。 たとえば、次のスニペットでは、リストを繰り返し処理し、すべての偶数要素を数値0に置き換えています。

fun replaceEvenNumbersBy0Iterator(list: MutableList<Int>): MutableList<Int> {
    val iterator = list.listIterator()
    while(iterator.hasNext()) {
        val value = iterator.next()
        if (value % 2 == 0) {
            iterator.set(0)
        }
    }

    return list
}

まず、 mutableListOf 組み込みメソッドを使用して可変リストを作成し、 listIterator()メソッドを実行してイテレーターを取得します。

次に、要素が偶数であるかどうかを確認するだけです。 そうである場合は、 set メソッドを使用して、最後に取得された要素を変更します。

4. リストセッターを使用してインプレースで変更

リストの要素をインプレースで変更する別の方法は、特定のインデックスの値を設定することです for ループを使用して、0からN– 1 まで繰り返します。ここで、Nはリスト内の要素の数です。

次に、条件を確認し、インデックス番号を使用してその位置に値を直接設定します。

fun replaceEvenNumbersBy0Direct(list: MutableList<Int>): MutableList<Int> {
    for (i in 0 until list.size) {
        val value = list[i]
        if (value % 2 == 0) {
            list[i] = 0
        }
    }

    return list
}

5. 拡張メソッドの作成

Kotlinでは、デコレータクラスを継承または作成せずに、クラスの機能を拡張することができます。 これは、拡張機能と呼ばれる関数の特別な宣言によって行われます。

MutableList クラスに拡張メソッドを作成し、リストのすべての要素に操作を適用できるようにします。 メソッドはパラメーターとして関数を受け取り、この関数をリスト内のすべての要素に適用します。

例を考えてみましょう:

fun <Int> MutableList<Int>.mapInPlace(mutator: (Int) -> (Int)) {
    this.forEachIndexed { i, value ->
        val changedValue = mutator(value)

        this[i] = changedValue
    }
}

アプリケーションのコンテキスト内で使用できるMutableListクラスのInt要素に対してmapInPlaceという新しいメソッドを作成しています。 このようにして、 MutableList ソースコードの変更を回避し、選択した場所に含めることができます。

メソッドは、リスト内のすべての要素に適用される関数であるミューテーターパラメーターを期待します。

ミューテーターは、条件に基づいて、提供されたリストの要素を変更する必要があるかどうかを確認します。 ミューテーター関数の例を見てみましょう。

fun <Int> MutableList<Int>.mapInPlace(mutator: (Int) -> (Int)) {
    this.forEachIndexed { i, value ->
        val changedValue = mutator(value)

        this[i] = changedValue
    }
}

val list = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
list.mapInPlace {
    element -> if (element % 2 == 0) 0 else element
}
// [1, 0, 3, 0, 5, 0, 7, 0, 9, 0]
println(list)

ご覧のとおり、リスト内のすべての要素について、それが偶数であるかどうかを確認します。 その場合は0を返し、そうでない場合は変更せずに要素を返します。 この戻り値は、リストの新しい要素として使用されます。

Kotlinキーワードitを使用して、角かっこ内の要素にアクセスすることもできます。

fun <Int> MutableList<Int>.mapInPlace(mutator: (Int) -> (Int)) {
    this.forEachIndexed { i, value ->
        val changedValue = mutator(value)

        this[i] = changedValue
    }
}

val list = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
list.mapInPlace {
     if (it % 2 == 0) 0 else it
}
// [1, 0, 3, 0, 5, 0, 7, 0, 9, 0]
println(list)

mapInPlace 関数で考えられる改善点は、要素を挿入する前に、要素が実際に変更されているかどうかを確認することです。 ミューテイタ関数によって返される値を現在の要素と単純に比較できます。

fun <Int> MutableList<Int>.mapInPlace(mutator: (Int) -> (Int)) {
    this.forEachIndexed { i, value ->
        val changedValue = mutator(value)

        if (value != changedValue) {
            this[i] = changedValue
        }
    }
}

この関数は、任意のタイプのリストで機能するように簡単に一般化できます。

fun <T> MutableList<T>.mapInPlace(mutator: (T) -> (T)) {
    this.forEachIndexed { i, value ->
        val changedValue = mutator(value)

        if (value != changedValue) {
            this[i] = changedValue
        }
    }
}

配列クラスで拡張メソッドを使用することもできます。

fun <T> Array<T>.mapInPlace(mutator: (T) -> (T)) {
    this.forEachIndexed { i, value ->
        val changedValue = mutator(value)

        if (value != changedValue) {
            this[i] = changedValue
        }
    }
}

6. 結論

この記事では、さまざまな方法を適用してリストを繰り返しながら、リストを更新する方法を説明しました。

いつものように、コードスニペットはGitHubにあります。