Использование строителей с выводом типа строителя

Kotlin поддерживает определение типа строителя (или вывод строителя), которое может оказаться полезным при работе с универсальными строителями. Это помогает компилятору определять тип аргументов вызова строителя на основе информации о типе других вызовов внутри его лямбда-аргумента.

Рассмотрим в качестве примера использование buildMap().

fun addEntryToMap(baseMap: Map<String, Number>, additionalEntry: Pair<String, Int>?) {
   val myMap = buildMap {
       putAll(baseMap)
       if (additionalEntry != null) {
           put(additionalEntry.first, additionalEntry.second)
       }
   }
}

Здесь недостаточно информации о типе для обычного определения типа аргументов, но вывод строителя может анализировать вызовы внутри аргумента лямбда. Основываясь на информации о типе вызовов putAll() и put(), компилятор может автоматически выводить тип аргументов вызова buildMap() как String и Number. Вывод строителя позволяет опускать аргументы типа при использовании универсальных строителей.

Создание ваших собственных строителей

Требования для включения вывода строителя

До Kotlin 1.6.0 для включения вывода строителя для функции строителя требовалось аннотация @BuilderInference в лямбда-параметре строителя. В версии 1.6.0 вы можете её опустить, если и вы, и клиенты вашего строителя используете опцию компилятора -Xenable-builder-inference.

Чтобы вывод строителя работал для вашего собственного строителя, убедитесь, что в его объявлении есть лямбда-параметр типа функции с получателем. Существует также два требования к типу приемника:

  1. Он должен использовать тип аргументов, который должен определить вывод строителя. Например:

    fun <V> buildList(builder: MutableList<V>.() -> Unit) { ... }
    

    Обратите внимание, что передача типов параметра напрямую, например fun <T> myBuilder(builder: T.() -> Unit), еще не поддерживается.

  1. Он должен предоставлять public члены или расширения, которые содержат соответствующие типы параметра в своей сигнатуре. Например:

    class ItemHolder<T> {
        private val items = mutableListOf<T>()
    
        fun addItem(x: T) {
            items.add(x)
        }
    
        fun getLastItem(): T? = items.lastOrNull()
    }
       
    fun <T> ItemHolder<T>.addAllItems(xs: List<T>) {
        xs.forEach { addItem(it) }
    }
    
    fun <T> itemHolderBuilder(builder: ItemHolder<T>.() -> Unit): ItemHolder<T> = 
        ItemHolder<T>().apply(builder)
    
    fun test(s: String) {
        val itemHolder1 = itemHolderBuilder { // Тип itemHolder1 - это ItemHolder<String>
            addItem(s)
        }
        val itemHolder2 = itemHolderBuilder { // Тип itemHolder2 - это ItemHolder<String>
            addAllItems(listOf(s)) 
        }
        val itemHolder3 = itemHolderBuilder { // Тип itemHolder3 - это ItemHolder<String?>
            val lastItem: String? = getLastItem()
            // ...
        }
    }
    

Поддерживаемые возможности

Вывод строителя поддерживает:

  • определение нескольких типов аргументов,

    fun <K, V> myBuilder(builder: MutableMap<K, V>.() -> Unit): Map<K, V> { ... }
    
  • определение типа аргументов нескольких лямбд строителя в одном вызове, включая взаимозависимые,

    fun <K, V> myBuilder(
        listBuilder: MutableList<V>.() -> Unit,
        mapBuilder: MutableMap<K, V>.() -> Unit
    ): Pair<List<V>, Map<K, V>> =
        mutableListOf<V>().apply(listBuilder) to mutableMapOf<K, V>().apply(mapBuilder)
      
    fun main() {
        val result = myBuilder(
            { add(1) },
            { put("key", 2) }
        )
        // result имеет типы Pair<List<Int>, Map<String, Int>>
    }
    
  • определение типа аргументов, типы параметров которых являются типами параметров лямбды или возвращаемых значений,

    fun <K, V> myBuilder1(
        mapBuilder: MutableMap<K, V>.() -> K
    ): Map<K, V> = mutableMapOf<K, V>().apply { mapBuilder() }
      
    fun <K, V> myBuilder2(
        mapBuilder: MutableMap<K, V>.(K) -> Unit
    ): Map<K, V> = mutableMapOf<K, V>().apply { mapBuilder(2 as K) }
      
    fun main() {
        // тип result1 определен как Map<Long, String>
        val result1 = myBuilder1 {
            put(1L, "value")
            2
        }
        val result2 = myBuilder2 {
            put(1, "value 1")
            // You can use `it` as "postponed type variable" type
            // See the details in the section below
            put(it, "value 2")
        }
    }
    

Как работает вывод строителя

Переменные отложенного типа

Вывод строителя работает на основе переменных отложенного типа, которые появляются внутри лямбды строителя во время анализа вывода строителя. Переменная отложенного типа - это тип аргумента, который находится в процессе вывода. Компилятор использует его для сбора информации о типе аргумента.

Рассмотрим пример с buildList():

val result = buildList {
    val x = get(0)
}

Здесь x имеет тип переменной отложенного типа: вызов get() возвращает значение типа E, но само E еще не уточнено. На данный момент конкретный тип для E неизвестен.

Когда значение переменной отложенного типа связывается с конкретным типом, вывод строителя собирает эту информацию для вывода результирующего типа соответствующего аргумента в конце анализа вывода строителя. Например:

val result = buildList {
    val x = get(0)
    val y: String = x
} // result имеет выведенный тип List<String>

После того как переменная отложенного типа присваивается переменной типа String, вывод строителя получает информацию о том, что x является подтипом String. Это присваивание является последним оператором в лямбде строителя, поэтому анализ вывода строителя заканчивается результатом вывода аргумента E в String.

Обратите внимание, что вы всегда можете вызвать функции equals(), hashCode() и toString() с переменной отложенного типа в качестве получателя.

Вклад в результаты вывода строителя

Вывод строителя может собирать различную информацию о типе, которые влияют на результат анализа. Он рассматривает:

  • Вызов методов в приемнике лямбды, которые используют тип параметра;

    val result = buildList {
        // Тип аргумента выводится в String на основе переданного аргумента "value"
        add("value")
    } // result имеет выведенный тип List<String>
    
  • Указание ожидаемого типа для вызовов, возвращающих тип параметра;

    val result = buildList {
        // Тип аргумента выводится в Float на основе ожидаемого типа
        val x: Float = get(0)
    } // result имеет тип List<Float>
    
    class Foo<T> {
        val items = mutableListOf<T>()
    }
    
    fun <K> myBuilder(builder: Foo<K>.() -> Unit): Foo<K> = Foo<K>().apply(builder)
    
    fun main() {
        val result = myBuilder {
            val x: List<CharSequence> = items
            // ...
        } // result имеет тип Foo<CharSequence>
    }
    
  • Передачу типов переменных отложенного типа в методы, которые ожидают конкретных типов;

    fun takeMyLong(x: Long) { /*...*/ }
    
    fun String.isMoreThat3() = length > 3
    
    fun takeListOfStrings(x: List<String>) { /*...*/ }
    
    fun main() {
        val result1 = buildList {
            val x = get(0)
            takeMyLong(x)
        } // result1 имеет тип List<Long>
    
        val result2 = buildList {
            val x = get(0)
            val isLong = x.isMoreThat3()
        // ...
        } // result2 имеет тип List<String>
      
        val result3 = buildList {
            takeListOfStrings(this)
        } // result3 имеет тип List<String>
    }
    
  • Получение вызываемой ссылки на член лямбда-получателя.

    fun main() {
        val result = buildList {
            val x: KFunction1<Int, Float> = ::get
        } // result имеет тип List<Float>
    }
    
    fun takeFunction(x: KFunction1<Int, Float>) { ... }
    
    fun main() {
        val result = buildList {
            takeFunction(::get)
        } // result имеет тип List<Float>
    }
    

В конце анализа вывод строителя рассматривает всю собранную информацию о типе и пытается объединить ее в результирующий тип. Смотрите пример.

val result = buildList { // Вывод переменной отложенного типа E
    // Учитывание, что E - это Number или подтип Number
    val n: Number? = getOrNull(0)
    // Учитывание, что E - это Int или супертип Int
    add(1)
    // E выводится в Int
} // result имеет тип List<Int>

Результирующий тип является наиболее специфичным типом, который соответствует информации о типе, собранной в ходе анализа. Если данная информация о типе противоречива и не может быть объединена, компилятор сообщает об ошибке.

Обратите внимание, что компилятор Kotlin использует вывод строителя только в том случае, если обычный вывод типа не может вывести аргумент типа. Это означает, что вы можете вносить информацию о типе за пределами лямбды строителя, и тогда анализ вывода строителя не требуется. Рассмотрим пример:

fun someMap() = mutableMapOf<CharSequence, String>()

fun <E> MutableMap<E, String>.f(x: MutableMap<E, String>) { ... }

fun main() {
    val x: Map<in String, String> = buildMap {
        put("", "")
        f(someMap()) // Несоответствие типа (требуется String, найдено CharSequence)
    }
}

Здесь возникает несоответствие типов, поскольку ожидаемый тип map указан вне лямбды конструктора. Компилятор анализирует все операторы внутри с фиксированным типом получателя Map<in String, String>.