[Kotlin]ビルダー関数によるコレクション生成/そのハマりポイント

Kotlin1.3.70から追加された新しい機能の一部としてbuildList、buildSet、buildMapがあります。これらの関数の特徴は、レシーバをラムダ式の中でだけミュータブルとして扱うという点です。

今回はこれらのビルダー関数を使用して、コレクションを生成してみたいと思います。

buildListの実装

@ExperimentalStdlibApi inline fun <E> buildList(
    builderAction: MutableList<E>.() -> Unit   //ラムダ式
): List<E>
@ExperimentalStdlibApi inline fun <E> buildList(
    capacity: Int,   //要素数を指定
    builderAction: MutableList<E>.() -> Unit   //ラムダ式
): List<E>

APIリファレンスを見てみると、

  1. インライン関数である
  2. 引数はラムダ式(builderAction)のみか、要素数Intとラムダ式かの2択
  3. ラムダ式の戻り値はUnit
  4. 関数の最終的な戻り値はList

であることが分かります。

ビルダー関数は今のところ実験的な実装のため、「@ExperimentalStdlibApi」(実験的な標準関数API)アノテーションが付いています。利用するには「@OptIn(ExperimentalStdlibApi::class)」を付け加えましょう。(現在は無くても動きます)

buildListの使用例

では実際に使用してみます。

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    val list1 = listOf(1,2,3)

    val list2 = buildList {
        this.add(3)       //list2をthisとして参照
        add(4)            //thisは省略可能
        addAll(list1)     //list2 = [3,4,1,2,3]
    }.distinct().sorted()  //最終的な戻り値は読み込み専用List。sort()は不可

    println(list2)
}
//[1, 2, 3, 4]

this.add(3)からaddAll(list1)までがラムダ式内の処理です。ここまでは変数list2はMutableListですが、ラムダ式を出た瞬間イミュータブルなListに変化します。

ラムダ式内部でdistinct()sorted()など、Listを返すようなメソッドは使えないことに注意しましょう。

引数capacityを指定する

buildListの引数にはもう1つ、「capacity」があります。この引数は最終的な戻り値であるListの要素数を示しますが、

val list2 = buildList(1) {...}
println(list2)  //[1, 2, 3, 4]

例えばcapacityを1と指定しても、Listの要素数は変化しません。公式ではこんな説明がされています。

capacity is used to hint the expected number of elements added in the builderAction.

(capacityは、ラムダ式によって追加されるであろう要素数を明示するために利用される)

つまりこの引数を指定したからといって、戻り値の最終的な要素数が変化したりすることはありません。

なおこのcapacityが負の値になった場合、「IllegalArgumentException」(不正な引数の例外)が送出されます。

buildMapのハマりポイント

buildSetやbuildMapも基本的にはListのときと同じ要領で利用できます。例としてMapを生成してみましょう。

@OptIn(ExperimentalStdlibApi::class)
fun main() {
    val list1 = listOf(1,2,3)
    val list2 = listOf("あつ森","FF7","バイオ")

    val map = buildMap<Int, String>{   //型を明示する
        put(0, "PUBG")
        list1.forEach{

            //thisはMutableMap、itはforEachの呼び出し元
            this[it] = list2[it - 1]
        }
    }
    println(map)
}
//{0=PUBG, 1=あつ森, 2=FF7, 3=バイオ}

特にMapを生成するときに気を付けたいポイントとして、ラムダ式に含まれるメソッドによってはbuildMap<Int, String>明示的な型指定が必要になる場合が多いです。注意してください。
この指定を外すと、

こんな警告が出ます。そのまま実行するとエラー。

Type inference failed: Not enough information to infer parameter K in inline fun <K, V> buildMap(builderAction: MutableMap<K, V>.() -> Unit): Map<K, V> Please specify it explicitly.

型推論の失敗: buildMapへ渡される情報が不足しているため、明示的な指定が必要である

buildMapでは明示的な型指定が無いとputメソッドすら使用できない状態です。そのくせ型指定をするとIDEに「冗長な表現」と言われます。

この挙動はおそらくバグではないかと思いますが、今のところbuild系の関数でList、Set、Mapを生成する場合、このようなエラーが起こったときはこの「明示的な型指定」が原因かもしれません。一度確認してみてください。

タイトルとURLをコピーしました