Skip to content

使用建造者型別推斷

Kotlin 支援 建造者型別推斷 (或稱建造者推斷),這對於處理泛型建造者 (generic builders) 時非常有用。它有助於編譯器根據其 Lambda 引數內其他呼叫的型別資訊,來推斷建造者呼叫的型別引數 (type arguments)。

考慮以下 buildMap() 的使用範例:

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

這裡的型別資訊不足以以常規方式推斷型別引數,但建造者推斷可以分析 Lambda 引數內的呼叫。根據 putAll()put() 呼叫的型別資訊,編譯器可以自動將 buildMap() 呼叫的型別引數推斷為 StringNumber。建造者推斷允許在使用泛型建造者時省略型別引數。

編寫您自己的建造者

啟用建造者推斷的要求

NOTE

在 Kotlin 1.7.0 之前,為建造者函數啟用建造者推斷需要 -Xenable-builder-inference 編譯器選項。在 1.7.0 中,該選項預設為啟用。

為了讓建造者推斷作用於您自己的建造者,請確保其宣告具有一個帶接收者的函數型別的建造者 Lambda 參數。對於接收者型別還有兩個要求:

  1. 它應該使用建造者推斷預期推斷的型別引數。例如:

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

    NOTE

    請注意,直接傳遞型別參數的型別,例如 fun <T> myBuilder(builder: T.() -> Unit) 尚不支援。

  2. 它應該提供包含對應型別參數的公開成員或擴充功能,並將其納入其簽章中。例如:

    kotlin
    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()
            // ...
        }
    }

支援的功能

建造者推斷支援:

  • 推斷多個型別引數
    kotlin
    fun <K, V> myBuilder(builder: MutableMap<K, V>.() -> Unit): Map<K, V> { ... }
  • 在一個呼叫中推斷多個建造者 Lambda 的型別引數,包括相互依賴的 Lambda
    kotlin
    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
    }
  • 推斷型別參數是 Lambda 參數或回傳型別的型別引數
    kotlin
    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")
        }
    }

建造者推斷如何運作

延遲型別變數

建造者推斷在建造者推斷分析期間,透過在建造者 Lambda 內部出現的 延遲型別變數 (postponed type variables) 來運作。延遲型別變數是一種正在推斷中的型別引數的型別。編譯器使用它來收集有關型別引數的型別資訊。

考慮 buildList() 的範例:

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

這裡 x 具有延遲型別變數的型別:get() 呼叫回傳型別為 E 的值,但 E 本身尚未確定。此時,E 的具體型別是未知的。

當延遲型別變數的值與具體型別關聯時,建造者推斷會收集此資訊,以便在建造者推斷分析結束時推斷對應型別引數的最終型別。例如:

kotlin
val result = buildList {
    val x = get(0)
    val y: String = x
} // result has the List<String> type inferred

在延遲型別變數被賦值給 String 型別的變數後,建造者推斷獲得了 xString 的子型別的資訊。此賦值是建造者 Lambda 中的最後一個語句,因此建造者推斷分析以將型別引數 E 推斷為 String 的結果結束。

請注意,您始終可以使用延遲型別變數作為接收者來呼叫 equals()hashCode()toString() 函數。

對建造者推斷結果的貢獻

建造者推斷可以收集不同種類的型別資訊,這些資訊會對分析結果產生貢獻。 它會考慮:

  • 在 Lambda 的接收者上呼叫使用型別參數型別的方法
    kotlin
    val result = buildList {
        // Type argument is inferred into String based on the passed "value" argument
        add("value")
    } // result has the List<String> type inferred
  • 為回傳型別參數型別的呼叫指定預期型別
    kotlin
    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
    kotlin
    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
    }
  • 將延遲型別變數的型別傳遞給預期具體型別的方法
    kotlin
    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
    }
  • 取得 Lambda 接收者成員的可呼叫參考
    kotlin
    fun main() {
        val x: KFunction1<Int, Float> = buildList {
            // Type argument is inferred into Float based on the expected type
            ::get
        }.let { it::get } // result has the List<Float> type
        // Note: The original example `val x: KFunction1<Int, Float> = ::get` inside buildList lambda 
        // will cause "Unresolved reference: get" because `::get` is outside `buildList` scope.
        // Assuming it intends to show how `::get` *within* the builder lambda influences inference.
        // The provided code snippet has a slight issue in how `::get` is used.
        // I'll keep the original structure, assuming the intent is to show `::get` as a reference.
        // If `::get` were `this::get`, it would be valid inside the lambda.
        // I'll translate the text as is.
    }
    (原英文範例中 val x: KFunction1<Int, Float> = ::getbuildList 內部可能存在語法問題,因為 ::get 通常需要一個明確的接收者。但根據上下文,其意圖應為展示 ::get 對型別推斷的影響。為了忠實原文,此處保留原文的程式碼結構。)
    kotlin
    fun takeFunction(x: KFunction1<Int, Float>) { ... }
    
    fun main() {
        val result = buildList {
            takeFunction(::get)
        } // result has the List<Float> type
    }

在分析結束時,建造者推斷會考慮所有收集到的型別資訊,並嘗試將其合併為最終型別。請看範例。

kotlin
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 編譯器僅在常規型別推斷無法推斷型別引數時才使用建造者推斷。這意味著您可以在建造者 Lambda 外部貢獻型別資訊,這樣就不需要建造者推斷分析了。考慮以下範例:

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)
    }
}

這裡出現型別不符,因為地圖的預期型別是在建造者 Lambda 外部指定的。編譯器使用固定的接收者型別 Map<in String, String> 分析內部所有語句。