5.7.3.2 使用构建器类型推断的构建器

原文链接: https://kotlinlang.org/docs/using-builders-with-builder-inference.html

5.7.3.2 使用构建器类型推断的构建器

Kotlin 支持构建器类型推断(builder type inference,简称构建器推断),在你使用泛型构建器时它会很有用。它可以根据构建器 lambda 实参内部其他调用的类型信息,帮助编译器推断构建器调用的类型实参。

看一下这个使用 buildMap() 的例子:

1
2
3
4
5
6
7
8
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 中该选项已默认启用。

要让构建器推断对你自己的构建器生效,请确保它的声明中有一个接收者函数类型的构建器 lambda 参数。此外,对接收者类型还有两项要求:

  1. 它应当使用构建器推断要推断的那些类型实参。例如:
1
   fun <V> buildList(builder: MutableList<V>.() -> Unit) { ... }

注意: 注意,像 fun <T> myBuilder(builder: T.() -> Unit) 这样直接传入类型参数的类型,目前还不支持。

  1. 它应当提供在其签名中包含相应类型参数的公开成员或扩展。例如:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
   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()
           // ...
       }
   }

支持的特性

构建器推断支持:

  • 推断多个类型实参
1
  fun <K, V> myBuilder(builder: MutableMap<K, V>.() -> Unit): Map<K, V> { ... }
  • 在一次调用中推断多个构建器 lambda 的类型实参,包括相互依赖的情况
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  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 的参数类型或返回类型的那些类型实参
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
  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 variable)工作,它们会在构建器推断分析期间出现在构建器 lambda 内部。延迟类型变量是正在推断过程中的类型实参。编译器用它来收集关于该类型实参的类型信息。

来看使用 buildList() 的例子:

1
2
3
val result = buildList {
    val x = get(0)
}

这里 x 的类型就是延迟类型变量:get() 调用返回类型为 E 的值,但 E 本身尚未确定。此时 E 的具体类型还是未知的。

当延迟类型变量的值与某个具体类型关联起来时,构建器推断会收集这一信息,以便在构建器推断分析结束时推断出相应类型实参的最终类型。例如:

1
2
3
4
val result = buildList {
    val x = get(0)
    val y: String = x
} // result 被推断为 List<String> 类型

在延迟类型变量被赋给 String 类型的变量之后,构建器推断就得到了 x 是 String 的子类型这一信息。这个赋值是构建器 lambda 中的最后一条语句,因此构建器推断分析结束时,类型实参 E 被推断为 String。

注意,你总是可以把延迟类型变量作为接收者调用 equals()、hashCode() 和 toString() 函数。

为构建器推断结果提供信息

构建器推断可以收集多种类型信息,它们都会影响分析结果。它会考虑:

  • 在 lambda 的接收者上调用使用了类型参数类型的方法
1
2
3
4
  val result = buildList {
      // 根据传入的 "value" 实参,类型实参被推断为 String
      add("value")
  } // result 被推断为 List<String> 类型
  • 为返回类型参数类型的调用指定期望类型
1
2
3
4
  val result = buildList {
      // 根据期望类型,类型实参被推断为 Float
      val x: Float = get(0)
  } // result 的类型是 List<Float>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  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>
  }
  • 把延迟类型变量的类型传给期望具体类型的方法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
  fun takeMyLong(x: Long) { ... }

  fun String.isMoreThan3() = 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.isMoreThan3()
      // ...
      } // result2 的类型是 List<String>

      val result3 = buildList {
          takeListOfStrings(this)
      } // result3 的类型是 List<String>
  }
  • 获取指向 lambda 接收者成员的可调用引用
1
2
3
4
5
  fun main() {
      val result = buildList {
          val x: KFunction1<Int, Float> = ::get
      } // result 的类型是 List<Float>
  }
1
2
3
4
5
6
7
  fun takeFunction(x: KFunction1<Int, Float>) { ... }

  fun main() {
      val result = buildList {
          takeFunction(::get)
      } // result 的类型是 List<Float>
  }

分析结束时,构建器推断会汇总收集到的所有类型信息,并尝试合并为最终类型。请看这个例子。

1
2
3
4
5
6
7
val result = buildList { // 推断延迟类型变量 E
    // 认为 E 是 Number 或 Number 的子类型
    val n: Number? = getOrNull(0)
    // 认为 E 是 Int 或 Int 的超类型
    add(1)
    // E 被推断为 Int
} // result 的类型是 List<Int>

最终类型是与分析期间收集到的类型信息相对应的最具体类型。如果给定的类型信息彼此矛盾、无法合并,编译器会报错。

注意,只有当常规类型推断无法推断出类型实参时,Kotlin 编译器才会使用构建器推断。这意味着你可以在构建器 lambda 之外提供类型信息,此时就不需要进行构建器推断分析了。看这个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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)
    }
}

这里出现类型不匹配,是因为映射的期望类型是在构建器 lambda 之外指定的。编译器会用固定的接收者类型 Map<in String, String> 分析内部的全部语句。