为什么 Kotlin 中有 reified 关键字,标记函数内联还不够吗?

Why have the reified keyword in Kotlin, isn't marking a function inline sufficient?

在Kotlin中,鉴于'reified'关键字只能用于内联函数的泛型类型参数,为什么要有reified关键字呢?为什么 Kotlin 编译器(至少在未来)不能自动将内联函数的所有泛型类型参数视为具体化?

我看到有人看到这个'reified'字就慌了,求我不要把代码搞复杂。因此问题。

具体化的类型参数要求传递给它们的类型参数也被具体化。有时这是一个不可能的要求(例如,class 参数不能具体化),因此默认情况下具体化内联函数的所有参数将导致无法调用所有内联函数,在现在只能调用不可能的情况下具有具体化类型参数的:

inline fun<T> genericFun(x: T)  {}
inline fun<reified T> reifiedGenericFun(x: T)  {}

class SimpleGenericClass<T>() {
    fun f(x: T) {
        genericFun<T>(x)        //compiles fine
        reifiedGenericFun<T>(x) //compilation error
    }
}

更新。为什么不根据上下文自动推断“reifibility”?

  1. 方法 1(@Tenfour04 建议):分析内联函数的代码,如果它有 T::class 个调用,则将其类型参数视为具体化(我还添加了 is T 来电)。
  2. 方法 2(由@SillyQuestion 建议):将内联函数的所有类型参数视为默认具体化;如果它导致使用站点编译错误,则回退到非具体化类型。

这是两者的反例:"a" as? T。具有此主体的函数将具有不同的语义,具体取决于其类型参数是否被声明(或假设地推断)为具体化:

inline fun<reified T> castToReifiedGenericType() = "a" as? T
inline fun<T> castToSimpleGenericType() = "a" as? T

fun main() {
    println(castToReifiedGenericType<Int>()) //null
    println(castToSimpleGenericType<Int>())  //a
}

/*P.S. unsafe cast ("a" as T) also have different semantics for reified and non-reified T, 
causing `ClassCastException` in the first case and still returning "a" in the latter.*/

因此,对于第一种方法,如果我们在内联函数的某处添加对 T::class/is T 的无意义调用,语义将会改变。对于第二个 - 如果我们从新站点调用此函数(其中 T 不能是 reified,而它之前是“可具体化的”),语义将会改变,或者,相反地,从此删除调用站点(允许它是 reified)。

来自这些操作的调试问题(乍一看与观察语义变化无关)比 adding/reading 一个明确的 reified 关键字更复杂,更容易引起恐慌。

正如@Михаил Нафталь 的回答所表明的那样,reified 的类型非常有限,因此语言要求您明确指出应该具体化哪些类型是至关重要的。具体化要求在编译时知道类型,因此只能从该类型不是非具体化泛型的函数中调用具有具体化类型的函数。

有人可能会争辩说,那么,只有在 T::class 恰好在此内联函数内部使用时才假设它是具体化的,否则将其视为未具体化。但这意味着您的有效函数签名可以通过更改函数的内容而不更改其声明来更改。这将使意外更改函数签名变得非常容易,这在未来是灾难的根源。例如,假设我有这个功能:

inline fun <T> registerList(list: List<T>) { // T is not reified
    myProtectedRegistryList.add(list)
}

所以它在我的应用程序的其他地方使用,或者像这样被我的图书馆的用户使用:

class Foo<T>(val data: List<T>) {
    init { 
        registerList(data)
    }
}

// or

fun foo(data: List<T>) {
    registerList(data)
}

// or

class Bar<T> {
    var barRegister: (List<T>)->Unit = ::registerList
}

稍后我在不更改其声明的情况下修改我的函数:

// In hypothetical Kotlin with implicit reification, T is now reified:
inline fun <T> registerList(list: List<T>) { // This line of code unchanged!
    myProtectedRegistryMap.put(T::class, list)
}

现在我已经破解了所有使用它的地方的代码,就像上面的一个例子一样。因此,通过要求您更改声明以更改签名的语言,您不得不考虑修改函数的外部影响。您知道,如果您重构任何函数的声明,那是其可用性在调用点受到影响的唯一方式。

Kotlin 在这种事情上的设计哲学是保守的,需要明确的声明。这与必须使用 fun 关键字显式标记功能接口的原因相同,即使它们当前只有一个抽象函数,并且默认情况下 classes/functions 是最终的。

接受@Михаил Нафталь(第一个提供并进一步更新)的回答,同时热烈感谢@Tenfour04。 只是想根据我对所提供答案的理解添加一个(希望)更简化的答案,这基本上仍然是正确的:

  • 'reifiable type' 的实用(但可能不完整)定义是,如果类型类似于 List<T>,我们可以从中计算 T::class.java。这种可具体化的类型可能没有对其进行类型擦除,这是 java 编译器对类型参数所做的事情,以减少编译二进制文件的大小。目前看来,在某些情况下,遗留 java 编译器等正在执行类型擦除,而 kotlin 尚未提供自定义方法(即在某些情况下阻止它)。
  • 不幸的是,仅从表达式 List<T> 无法确定该类型是否可具体化。就好像可以为开发人员添加一个关键字来明确地看到这一点,例如:List<nonerased T>List<erased T>。但是目前,编译器是根据where/how T 的定义来检测的,否则语言可能会变得过于冗长。
  • 在非内联函数中,所有泛型类型参数都变得不可具体化(擦除),可能是由于编译时 jvm 类型擦除,符合 java编译器。
  • 但是在内联函数中,例如inline fun <T> f1(list: List<T>){...},有一个选项可以将类型声明为 reified T(这使得它成为 inline fun <reified T> f1(list: List<T>){...}),这意味着它将只接受 List<nonerased T> 种参数(具有此不可见'nonerased' 与它们关联的关键字),否则会出现编译错误。然后它不会为 T 执行类型擦除。由于此编译时检查,用户现在可以继续在内联函数中使用 T::class.java,并确保他们不会在运行时出现错误,因为 T 是erased T 所以找不到 T::class.java
  • 没有 visible 'reified' 关键字的内联函数将像非内联函数一样执行类型擦除。