Spec-Zone.ru › Kotlin 1.9

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

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.7.0 включение вывода типа билдера для функции билдера требовало -Xenable-builder-inference параметр компилятора. В 1.7.0 этот параметр включён по умолчанию.

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

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

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

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

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

    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 { // Type of itemHolder1 is ItemHolder<String>
            addItem(s)
        }
        val itemHolder2 = itemHolderBuilder { // Type of itemHolder2 is ItemHolder<String>
            addAllItems(listOf(s)) 
        }
        val itemHolder3 = itemHolderBuilder { // Type of itemHolder3 is 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 has Pair<List<Int>, Map<String, Int>> type
    }
    
  • Вывод аргументов типа, чьи параметры типа являются параметрами или типами возвращаемых значений лямбда-выражения

    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 has the Map<Long, String> type inferred
        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 has the List<String> type inferred

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

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

Вклад в результаты вывода типа билдера

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

  • Вызов методов получателя лямбда-выражения, использующих тип параметра

    val result = buildList {
        // Type argument is inferred into String based on the passed "value" argument
        add("value")
    } // result has the List<String> type inferred
    
  • Указание ожидаемого типа для вызовов, возвращающих тип параметра

    val result = buildList {
        // Type argument is inferred into Float based on the expected type
        val x: Float = get(0)
    } // result has the List<Float> type
    
    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 has the Foo<CharSequence> type
    }
    
  • Передача типов отложенных переменных типа в методы, которые ожидают конкретные типы

    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 has the List<Long> type
    
        val result2 = buildList {
            val x = get(0)
            val isLong = x.isMoreThat3()
        // ...
        } // result2 has the List<String> type
    
        val result3 = buildList {
            takeListOfStrings(this)
        } // result3 has the List<String> type
    }
    
  • Получение ссылок на вызываемые объекты члена получателя лямбда-выражения

    fun main() {
        val result = buildList {
            val x: KFunction1<Int, Float> = ::get
        } // result has the List<Float> type
    }
    
    fun takeFunction(x: KFunction1<Int, Float>) { ... }
    
    fun main() {
        val result = buildList {
            takeFunction(::get)
        } // result has the List<Float> type
    }
    

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

val result = buildList { // Inferring postponed type variable E
    // Considering E is Number or a subtype of Number
    val n: Number? = getOrNull(0)
    // Considering E is Int or a supertype of Int
    add(1)
    // E gets inferred into Int
} // result has the List<Int> type

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

Обратите внимание, что компилятор 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()) // Type mismatch (required String, found CharSequence)
    }
}

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

Последнее изменение: 09 июня 2022
Безопасные для типа билдеры Безопасность от null

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/using-builders-with-builder-inference.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API