web-dev-qa-db-ja.com

Kotlinでは、繰り返しながらリストの内容をどのように変更しますか

リストがあります:

val someList = listOf(1, 20, 10, 55, 30, 22, 11, 0, 99)

そして、いくつかの値を変更しながらそれを繰り返したいと思います。 mapでできることはわかっていますが、リストのコピーが作成されます。

val copyOfList = someList.map { if (it <= 20) it + 20 else it }

コピーなしでこれを行うにはどうすればよいですか?

注:この質問は作者によって意図的に書かれ、回答されています(- Self-Answered Questions ) 、そのため、よく聞かれるKotlinのトピックに対する慣用的な回答がSOにあります。また、現在のKotlinでは正確ではないKotlinのアルファ用に書かれたいくつかの本当に古い回答を明確にするため。

28
Jayson Minard

まず、リストのすべてのコピーが悪いわけではありません。コピーはCPUキャッシュを利用して非常に高速になる場合があり、リスト、サイズ、およびその他の要因に依存します。

第二に、「インプレース」リストを変更するには、変更可能なタイプのリストを使用する必要があります。サンプルでは、​​listOfを使用します。これは_List<T>_インターフェイスを返し、読み取り専用です。可変リスト(つまり、ArrayList)のクラスを直接参照する必要があります。または、ヘルパー関数arrayListOfまたはlinkedListOfを使用して_MutableList<T>_ 参照。それができたら、突然変異メソッドlistIterator()を持つset()を使用してリストを繰り返すことができます。

_// create a mutable list
val someList = arrayListOf(1, 20, 10, 55, 30, 22, 11, 0, 99)

// iterate it using a mutable iterator and modify values 
val iterate = someList.listIterator()
while (iterate.hasNext()) {
    val oldValue = iterate.next()
    if (oldValue <= 20) iterate.set(oldValue + 20)
}
_

これにより、反復が発生するとリスト内の値が変更され、すべてのリストタイプに対して効率的です。これを簡単にするには、再利用できる便利な拡張機能を作成します(以下を参照)。

単純な拡張関数を使用した変更:

任意のMutableList実装に対してインプレース可変反復を実行するKotlinの拡張関数を作成できます。これらのインライン関数は、イテレータのカスタム使用と同じくらい速く実行され、パフォーマンスのためにインライン化されます。 Androidまたは任意の場所に最適です。

以下にmapInPlace拡張関数を示します(mapmapToなどのこれらのタイプの関数の典型的な命名を維持します):

_inline fun <T> MutableList<T>.mapInPlace(mutator: (T)->T) {
    val iterate = this.listIterator()
    while (iterate.hasNext()) {
        val oldValue = iterate.next()
        val newValue = mutator(oldValue)
        if (newValue !== oldValue) {
            iterate.set(newValue)
        }
    }
}
_

この拡張機能のバリエーションを呼び出す:

_val someList = arrayListOf(1, 20, 10, 55, 30, 22, 11, 0, 99)
someList.mapInPlace { if (it <= 20) it + 20 else it }
_

ほとんどのイテレータはremove()ではなくset()メソッドしか持たないため、これはすべての_Collection<T>_に対して一般化されていません。

配列の拡張関数

同様の方法で汎用配列を処理できます。

_inline fun <T> Array<T>.mapInPlace(mutator: (T)->T) {
    this.forEachIndexed { idx, value ->
        mutator(value).let { newValue ->
            if (newValue !== value) this[idx] = mutator(value)
        }
    }
}
_

そして、各プリミティブ配列に対して、次のバリエーションを使用します。

_inline fun BooleanArray.mapInPlace(mutator: (Boolean)->Boolean) {
    this.forEachIndexed { idx, value ->
        mutator(value).let { newValue ->
            if (newValue !== value) this[idx] = mutator(value)
        }
    }
}
_

参照平等のみを使用した最適化について

上記の拡張関数は、別のインスタンスに変更されていない場合は値を設定せず、_===_または_!==_の使用が Referential Equality であることを確認して、少し最適化します。 equals()またはhashCode()をチェックする価値はありません。これらを呼び出すとコストが不明になり、実際に参照等式が値を変更する意図をキャッチするためです。

拡張機能の単体テスト

以下に、機能する機能を示す単体テストケースと、コピーを作成するstdlib関数map()との小さな比較を示します。

_class MapInPlaceTests {
    @Test fun testMutationIterationOfList() {
        val unhappy = setOf("Sad", "Angry")
        val startingList = listOf("Happy", "Sad", "Angry", "Love")
        val expectedResults = listOf("Happy", "Love", "Love", "Love")

        // modify existing list with custom extension function
        val mutableList = startingList.toArrayList()
        mutableList.mapInPlace { if (it in unhappy) "Love" else it }
        assertEquals(expectedResults, mutableList)
    }

    @Test fun testMutationIterationOfArrays() {
        val otherArray = arrayOf(true, false, false, false, true)
        otherArray.mapInPlace { true }
        assertEquals(arrayOf(true, true, true, true, true).toList(), otherArray.toList())
    }

    @Test fun testMutationIterationOfPrimitiveArrays() {
        val primArray = booleanArrayOf(true, false, false, false, true)
        primArray.mapInPlace { true }
        assertEquals(booleanArrayOf(true, true, true, true, true).toList(), primArray.toList())
    }

    @Test fun testMutationIterationOfListWithPrimitives() {
        val otherList = arrayListOf(true, false, false, false, true)
        otherList.mapInPlace { true }
        assertEquals(listOf(true, true, true, true, true), otherList)
    }
}
_
56
Jayson Minard