【问题标题】:Why have the reified keyword in Kotlin, isn't marking a function inline sufficient?为什么在 Kotlin 中有 reified 关键字,标记函数内联不够?
【发布时间】:2022-01-21 14:16:53
【问题描述】:

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

我看到人们在看到这个“具体化”的词时会感到恐慌,并要求我不要让代码变得复杂。因此问题。

【问题讨论】:

  • 这能回答你的问题吗? How does the reified keyword in Kotlin work?
  • 我有点惊讶如果具体类型的概念非常复杂,那么有人可能会被聘用在编程工作中。
  • 这不是另一个问题的重复! 它询问为什么reified 关键字不能是隐式的设计推理。 OP 显然理解关键字的作用和含义。

标签: kotlin kotlin-reified-type-parameters


【解决方案1】:

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

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

与添加/读取显式 reified 关键字相比,调试来自这些操作的问题(乍一看与观察语义变化无关)要复杂得多,也更容易引发恐慌。

【讨论】:

  • 这个问题只暗示了那些允许在它们上使用“reify”关键字的参数。编译器是否能够知道何时无法具体化参数并且在这种情况下不具体化它?这样还是可以达到消除这个看似没有太大实际意义的“具体化”关键字的目的。
  • “不具体化”是什么意思?如果某些内联函数使用T 作为具体类型(不仅是T::class 调用,还有is T 检查和as T 强制转换),它应该如何处理“未具体化”参数?改用Any??显然,这没有任何意义。
  • 'not reified' 我的意思是:想象一下编译器像代码辅助工具一样工作:对于每个内联函数的类型参数,它会检查它是否自动添加'reified',是这样吗导致编译错误与否。如果导致编译错误,则不要添加'reified'关键字。
  • 如果自动添加'reified'关键字导致编译错误(在某些内联函数的使用站点),所以它不添加'reified'关键字,但它也会导致编译错误(内联函数本身)。编译器应该如何报告这个错误?在不知道开发人员的意图的情况下(用reified 关键字明确表达/没有它),编译器无法正确指向导致错误的行。
  • 另一个“房间里的大象”是 Java 互操作 - 不能从 Java 调用带有 reified 关键字的内联函数(它们被编译为 synthetic
【解决方案2】:

正如@Михаил Нафталь 的回答所表明的那样,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 关键字显式标记的原因相同,即使它们当前只有一个抽象函数,并且类/函数默认为最终函数。

【讨论】:

  • 已编辑,因为我最初回答时没有注意到其他答案。我已经删除了对同一件事的多余且更复杂的解释。
  • 您能否提供一个示例,通过更改函数的内容而不是其声明来可视化有效的函数签名如何变化?
  • 我添加了一个例子。
  • “代码已损坏”是指编译时错误吗?是不是二进制文件的接口发生了变化,所以客户端二进制文件不能与我们定义 registerList 的新二进制文件(jar 文件)一起工作?但是在那种情况下,为什么不能对所有可以应用 reified 关键字的类型都隐含 reified 关键字?这样就不会出现函数签名改变的问题了。
  • 是的,它会产生编译错误。如果始终强制执行 reified,那么在我上面展示的三种情况下,您永远不能使用内联泛型函数。正如我的回答所解释的那样,具体类型更加有限。
【解决方案3】:

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

  • “可具体化类型”的实用(但可能不完整)定义是,如果类型类似于List&lt;T&gt;,我们可以从中评估T::class.java。这种可具体化的类型可能没有对其进行类型擦除,这是 java 编译器对类型参数执行的操作,以减少编译后的二进制文件的大小。目前看来,在某些情况下,类型擦除是由传统 Java 编译器等完成的,而 kotlin 尚未提供自定义方法(即在某些情况下阻止它)。
  • 不幸的是,仅从表达式List&lt;T&gt; 无法判断该类型是否可具体化。好像可以为开发人员添加一个关键字以明确地看到这一点,例如:List&lt;nonerased T&gt;List&lt;erased T&gt;。但目前,编译器会根据 T 的定义位置/方式来检测这一点,否则语言可能会变得过于冗长。
  • 在非内联函数中,所有泛型类型参数都变得不可具体化(擦除),这可能是由于编译时 jvm 类型擦除,这与 Java 编译器的通常(或目前技术上尚未解决的)行为一致。
  • 但是在内联函数中,例如inline fun &lt;T&gt; f1(list: List&lt;T&gt;){...},可以选择将类型声明为reified T(使其成为inline fun &lt;reified T&gt; f1(list: List&lt;T&gt;){...}),这意味着它将仅接受List&lt;nonerased T&gt; 类型的参数(具有与它们相关联的这个不可见的'nonerased'关键字) , 否则会报编译错误。然后它不会对 T 进行类型擦除。由于此编译时检查,用户现在可以继续在内联函数中使用 T::class.java,并确保他们不会在运行时收到错误,因为 T 是 @987654331 @ 等T::class.java 找不到。
  • 没有可见 'reified' 关键字的内联函数将表现得像一个非内联函数,并像往常一样进行类型擦除。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-06-09
    • 1970-01-01
    • 2018-02-07
    • 2020-11-30
    • 2011-06-30
    • 2016-06-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多