第二个函数为您提供了一个在这种特殊情况下未使用的机会:它将接收器的类型捕获到类型参数T 中,以便您可以在其他地方使用它签名,例如在参数类型或返回值类型中,或在函数体中。
作为一个非常综合的示例,第二个函数中的listOf(this, this) 将被键入为List<T?>,保留项目类型与接收者类型相同的知识,而第一个函数中的相同表达式将是List<Any?>。
第一个函数不允许您一般使用接收器类型来存储该类型的项目,接受与参数相同类型的附加项目或在函数的返回值类型中使用接收器类型,而第二个函数允许所有这些。
这些函数从运行时的角度来看是等价的,就像编译代码时的generics are erased from the JVM bytecode一样,所以你将无法在运行时确定类型T并根据它进行操作,除非你将函数转换为一个inline function with a reified type parameter。
作为一个非常重要的特殊情况,将调用站点中的类型捕获到类型参数中允许高阶函数接受另一个在其签名中使用T 的函数。标准库有一组作用域函数(run、apply、let、also),它们显示了差异。
假设also 的签名没有使用泛型,看起来像这样:
fun Any?.also(block: (Any?) -> Unit): Any? { ... }
这个函数可以在任何对象上调用,但它的签名并没有表明它是传递给block并从函数返回的接收器对象——编译器将无法确保类型安全和,例如,允许在没有类型检查的情况下调用接收者对象的成员:
val s: String = "abc"
// won't compile: `it` is typed as `Any?`, the returned value is `Any?`, too
val ss1: String = (s + s).also { println(it.length) }
// this will work, but it's too noisy
val ss2: String = (s + s).also { println((it as String).length) } as String
现在,捕获类型参数正是表明它在所有三个地方都是相同类型的方式。我们修改签名如下:
fun <T : Any?> T.also(block: (T) -> Unit): T { ... }
编译器现在可以推断类型,知道它在任何地方出现的T都是相同的类型:
val s: String = "abc"
// OK!
val ss: String = (s + s).also { println(it.length) }