搭配建構器類型推論使用建構器 (Using builders with builder type inference)
Kotlin 支援建構器類型推論 (builder type inference,或稱建構器推論),當您使用泛型建構器時,它會非常有用。它可以幫助編譯器根據建構器 Lambda 參數中其他呼叫的類型資訊,推斷建構器呼叫的類型參數。
考慮以下 buildMap()
的使用範例:
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()
呼叫的類型參數為 String
和 Number
。建構器推論允許在使用泛型建構器時省略類型參數。
編寫您自己的建構器
啟用建構器推論的要求
在 Kotlin 1.7.0 之前,為建構器函數啟用建構器推論需要 -Xenable-builder-inference
編譯器選項。在 1.7.0 中,預設啟用此選項。
要讓建構器推論適用於您自己的建構器,請確保其宣告具有帶接收器 (receiver) 的函數類型的建構器 Lambda 參數。對於接收器類型也有兩個要求:
-
它應該使用建構器推論應該推斷的類型參數。例如:
fun <V> buildList(builder: MutableList<V>.() `->` Unit) { ... }
請注意,目前尚不支援直接傳遞類型參數的類型,例如
fun <T> myBuilder(builder: T.()
->Unit)
。 -
它應該提供在其簽名中包含相應類型參數的 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> { ... }
- 推斷一個呼叫中多個建構器 Lambda 的類型參數,包括相互依賴的類型參數
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>>
} - 推斷其類型參數是 Lambda 參數或返回類型的類型參數
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")
// 您可以使用 `it` 作為「延遲類型變數」類型
// 請參閱下面的章節了解詳細資訊
put(it, "value 2")
}
}
建構器推論如何運作
延遲類型變數 (Postponed type variables)
建構器推論以延遲類型變數 (postponed type variables) 的形式運作,這些變數在建構器推論分析期間出現在建構器 Lambda 內部。延遲類型變數是類型參數的類型,正在推斷過程中。編譯器使用它來收集有關類型參數的類型資訊。
考慮 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
的子類型 (subtype) 的資訊。此賦值是建構器 Lambda 中的最後一個語句,因此建構器推論分析以將類型參數 E
推斷為 String
的結果結束。
請注意,您始終可以呼叫帶有延遲類型變數作為接收器的 equals()
、hashCode()
和 toString()
函數。
貢獻於建構器推論結果
建構器推論可以收集有助於分析結果的不同種類的類型資訊。 它考慮:
- 呼叫 Lambda 接收器上使用類型參數類型的方法
val result = buildList {
// 類型參數根據傳遞的 "value" 參數推斷為 String
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> 類型
} - 取得對 Lambda 接收器的成員的可呼叫參考
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 的超類型 (supertype)
add(1)
// E 推斷為 Int
} // result 為 List<Int> 類型
結果類型是最符合在分析期間收集的類型資訊的特定類型。如果給定的類型資訊是矛盾的並且無法合併,則編譯器會報告錯誤。
請注意,僅當常規類型推論無法推斷類型參數時,Kotlin 編譯器才會使用建構器推論。這表示您可以在建構器 Lambda 外部提供類型資訊,然後不需要建構器推論分析。考慮以下範例:
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 的預期類型是在建構器 Lambda 外部指定的。編譯器使用固定的接收器類型 Map<in String, String>
分析所有語句。