Kotlin标准库里的那些internal注解

Author Avatar
Dexlind 7月 16, 2020
  • 在其它设备中阅读本文章

本文介绍一下 Koltin 标准库里面 kotlin.internal 这个包里的一些注解的作用。这些注解在 Kotlin 官网上的参考文档上没有任何提及,而且在网上搜也没有什么文章介绍,源码里的注释也只是寥寥几句话,难以理解。虽然是声明为 internal 的,但是许多开发者(包括一些 JetBrains 员工)在非 Kotlin 标准库的项目中也有使用。

本文使用的 Kotlin 版本为 1.3.72。如果您使用的是 1.4 版本的 Kotlin,并且想要验证本文代码中与类型推断有关的部分,请添加编译器参数 -XXLanguage:-NewInference 关闭充满 bug 的新类型推断算法!

另外由于文笔太差,本文不保证读者能看懂。

都有哪些注解

可以直接点开这个链接查看。

或者在IDEA里随便点开个 kt 文件,复制粘贴下面代码:

fun test() {
   kotlin.internal.NoInfer
}

光标定位到 NoInfer 处,按下 跳转到定义 的快捷键(Ctrl+B),就可以看到了。

如何使用这些注解

这些注解被声明为 internal,意味着该成员只在相同模块内可见,也就是只有 Kotlin 标准库能用这些注解。

这并不代表我们不能用他,有两种方法可以绕开限制。

其中一种方法我已经在之前介绍 Contracts DSL 的文章里提到过了,在自己的项目里新建一个 kt 文件,把这些注解的源码一个不漏地复制粘贴进去(包括包名),然后添加编译器参数 -Xallow-kotlin-package (允许使用 kotlin 开头的包名),就可以使用了。这里有个现成的例子可以模仿 orangy/squash

第二种方法是利用了我另一篇文章介绍的可以使用注解 @Suppress 强行消除编译错误。如下:

@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
fun <T> suppressTest(some: @kotlin.internal.NoInfer T) {}

个人比较推荐第二种方法。

Magic Inference Annotations

在大多数场景下,Kotlin 编译器的类型推断的默认行为足以满足需求。但是有时候会遇到一些例外情况需要我们对类型推断进行微调,于是就有了这些 magic inference annotations(官方就是这么称呼的)。

准确地说,它们都是对泛型的类型推导进行微调。

在介绍这些与类型推断有关的注解前先来了解一些 前 置 知 识,让我们来了解一下类型推断的大致流程(我会省略许多与本文无关的细节)。首先我们来看以下代码:

fun <T, R> some(t: T): R = TODO()

val result: String = some(1551)

编译器首先会从代码中获得一些信息,称为类型约束(type constraint)。代码中对 some 这个函数进行调用,将 1551 作为参数传给 some,把返回值赋值给类型为 String 的变量 result,编译器最终获得了两条约束:Int 是 T 的子类型,R 是 String 的子类型。(省略了很多细节)

然后由编译器的类型约束求解器(type constraint solver)对这些约束进行进一步的处理,最终获得类型推断的结果:T 推导为 Int,R 推导为 String。(省略了非常多的细节,怎样画马.jpg)

接下来开始正式介绍。

重新提一遍,如果您使用的是 1.4 版本的 Kotlin,并且想要验证本文代码,请添加编译器参数 -XXLanguage:-NewInference 关闭新类型推断算法!这个新类型推断算法对 magic inference annotations 的支持并不够好,还存在一些 bug。

(这里并不是说 1.3.72 版本的旧类型推断算法就没有 bug 了,只是本文的示例代码并没有涉及旧类型推断算法已知的 bug。)

@NoInfer

package kotlin.internal

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
internal annotation class NoInfer

Specifies that the corresponding type should be ignored during type inference.

kotlin.internal.NoInfer 比较简单,当一个类型被标注为 @NoInfer ,类型约束求解器并不会利用该类型提供的约束进行求解,编译器只检查类型推断的结果是否与约束相符合。简单地说就是不参与类型推断。比如下面的代码:

// 本文以后的代码默认带有这两行,@Suppress和import
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
import kotlin.internal.*

fun <T> someFun(t1: T, t2: @NoInfer T) {}

fun test() {
   someFun("str", 42) // 编译错误

   // someFun("str", "str2") // 这样是好的
}

someFun 的调用提供了两条类型约束: String 是 T 的子类型(来自第一个参数),Int 是 T 的子类型(来自第二个参数)。

但是因为 someFun 的定义里第二个参数的类型 T 上有 @NoInfer 注解,类型约束求解器无视了 “ Int 是 T 的子类型” 这条约束,仅使用第一条约束 “ String 是 T 的子类型” 进行求解,最终 T 推导成 String 类型。

编译器发现第二个参数的类型 Int 和类型推断的结果不符合:Int 不是 String 的子类型。于是编译错误就产生了。

PS:如果不加 @NoInfer 注解,那么类型约束求解器会根据那两条约束将 T 推导成 Any

再来看第二个例子:

fun <T> someFun2(t: @NoInfer T) {}

fun test2() {
   someFun2(42) // 编译错误

   // someFun2<Int>(42) // 这样是好的,而且IDEA不会跟你讲<Int>是多余的
}

在这个例子中,类型约束求解器实际上没有可用的类型约束,无法进行类型推断,需要在调用时显式传入泛型参数。

@Exact

package kotlin.internal

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.BINARY)
internal annotation class Exact

Specifies that the constraint built for the type during type inference should be an equality one.

@Exact 的注释在学习了上面的 前 置 知 识 后是非常好理解的。比如说这样的代码:

fun <T> someFun3(t: @Exact T) {}

fun test3() {
   someFun3(42)
}

根据 前 置 知 识,类型约束求解器会拿到一条 “ Int 是 T 的子类型” 的约束。但是 someFun3 的定义里参数 t 的类型上有 kotlin.internal.Exact 注解,这个注解会将这个约束修改成 “ T 和 Int 是相同的类型”(不是很严谨,但大概是这么个意思)。于是类型约束求解器不假思索很快就把 T 推导成 Int。

下面用一个例子来说明:

fun <T, U : T> someFun4(list: List<@Exact T>): U = TODO()

fun test4(list: List<Number>) {
   val i: Int = someFun4(list) // 这样是好的
   val s: String = someFun4(list) // 编译错误
}

someFun4 接收一个 List<T> 类型的参数,返回一个类型为 U 的对象,UT 的子类型,参数的类型 List<T>T 被标记为 @Exact

test4 里给 someFun4 传入List<Number>类型的参数,类型约束实际上是 “ List< Number> 是 List<@Exact T> 的子类型”。不过因为 Kotlin 的 List<E> 在类型参数 E 上是协变的,所以那条约束其实等价于 “ Number 是 @Exact T 的子类型”,最终被 @Exact 注解修正为 “ T 和 Number 是相同的类型”。(省略了大量编译器如何处理约束的细节)

所以在 test4 里的第一行 val i: Int = someFun4(list),有这样的约束:T 和 Number 是相同的类型,U 是 T 的子类型,U 是 Int 的子类型。最终结果为 T 推导为 Number、U 推导为 Int。

同理可知,第二行 val s: String = someFun4(list) 由于编译器找不到既是 Number 的子类型、又是 String 的子类型的类型,编译错误。

PS:如果不加 @Exact 注解,第二行的类型推断结果为 T 是 Any,U 是 String,不会有编译错误。

@OnlyInputTypes

package kotlin.internal

@Target(AnnotationTarget.TYPE_PARAMETER)
@Retention(AnnotationRetention.BINARY)
internal annotation class OnlyInputTypes

The value of this type parameter should be mentioned in input types (argument types, receiver type or expected type).

终于可以丢掉那些无聊又难搞的类型约束了。kotlin.internal.OnlyInputTypes 和上面那两个注解不同,它是标注在类型参数上的,如下:

fun <@OnlyInputTypes T> someFun5(t1: @NoInfer T, t2: @Exact T) {}

这个注解对类型推断的结果添加了一个限制,要求这个结果必须在 input types 的位置上被提及。就像这个注解的注释所说的,可以是 argument types,receiver type 或者 expected type。这几个位置可以用代码来简单展示:

val result: ExpectedType = receiverType.functionName(argumentType)

然后演示一下具体是什么意思:

fun <@OnlyInputTypes T> someFun6(t1: T, t2: T): T = TODO()

open class A
class B : A()
class C : A()

fun test6() {
   val r1 = someFun6(A(), B())    // 这个是好的
   val r2: A = someFun6(B(), C()) // 这个也是好的
   val r3 = someFun6(B(), C())    // 好不起来了!编译错误!
   someFun6<A>(B(), C())          // 又好起来了
}

显而易见,test6 里面前3次对 someFun6 的调用的类型推断结果都是 T 推导为 A 类型。

第一次函数调用,A 类型出现在 argument type 的位置,第二次 A 类型出现在 expected type 的位置,符合 @OnlyInputTypes 这个注解的要求,所以编译通过。

第三次调用由于 A 类型在 input types 的位置上没有被提及,所以编译失败了。

至于第四次调用则是纯粹抛弃了类型推导,手动指明了泛型参数,与 @OnlyInputTypes 没有关系了。

这里举一个 Kotlin 标准库里的例子来说明这个看起来有点“奇葩”的限制有什么用:

// 摘自 kotlin 标准库(有删减)

/**
 * Returns `true` if [element] is found in the array.
 */
fun <@OnlyInputTypes T> Array<out T>.contains(element: T): Boolean {
    return indexOf(element) >= 0
}

然后有时候不小心写出了这样的代码:

fun stupid(arr: Array<Int>) {
   val b = arr.contains("I want to find some String in an Int array. Oh! So stupid!")
   // 这里 T 推导为 Any
}

常识告诉我们,一个 Int 数组里不可能会有 String 元素,所以这行代码是无意义的。再看由于存在 @OnlyInputTypes 注解,类型推导的结果 Any 并没有出现在 input types 的位置上,所以编译失败了,从而阻止了这种无意义代码的产生。

PS:Kotlin 1.4 版本引入了一个默认开启的特性,将 @OnlyInputTypes 注解导致的编译错误(error)降级为警告(warring)。可以通过添加编译器参数 -XXLanguage:-NonStrictOnlyInputTypesChecks 关闭这个特性。(我并没有说一定要关闭这个特性)

PPS:可以在 Kotlin 标准库里搜索这三个注解都用在什么地方,理解这几个注解的实际用途。(我觉得都和我的举例差不多)

其他注解

@LowPriorityInOverloadResolution

package kotlin.internal

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
internal annotation class LowPriorityInOverloadResolution

Specifies that a corresponding member has the lowest priority in overload resolution.

是名字超长的 kotlin.internal.LowPriorityInOverloadResolution

我觉得它的名字和注释已经很好地解释了它的作用。

fun someFun7(i: Int?) {}

@LowPriorityInOverloadResolution
fun someFun7(i: Int) {}

fun test7() {
    someFun7(42) // 这将调用第一个someFun7
    ::someFun7   // 这将拿到第一个someFun7的引用
}

在标准库里多用于新旧 API 更替时,防止用户调用到旧的 API。

@HidesMembers

package kotlin.internal

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.BINARY)
internal annotation class HidesMembers

Specifies that the corresponding member has the highest priority in overload resolution.
Effectively this means that an extension annotated with this annotation will win in overload resolution over a member with the same signature.

kotlin.internal.HidesMembers 这个注解的名字和注释会给人造成一种错觉。

根据常识,Kotlin 的扩展函数不能覆盖成员函数:

fun Kotlin官网的例子() {
    class Example {
        fun printFunctionType() { println("Class method") }
    }

    fun Example.printFunctionType() { println("Extension function") }

    Example().printFunctionType() // 这将输出 Class method
}

但是 @HidesMembers 的确能让扩展函数覆盖成员函数:

class Box<T>(val t1: T, val t2: T) {
   fun forEach(fn: (T) -> Unit) {
      fn(t1)
      fn(t2)
   }
}

@HidesMembers
fun <T> Box<T>.forEach(fn: (T) -> Unit) {
   println("嘻嘻")
}

fun test8() {
   val box = Box(123, 456)
   box.forEach { println(it) } // 嘻嘻
}

你看 Kotlin 标准库里就是这么做的:

// kotlin标准库代码摘抄

@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

这是为了隐藏来自 java.lang.IterableforEach

不过这并不意味着我们可以随便用这个注解来做些七七八八的事情,在 Kotlin 编译器的源码里有这么一行代码:

// @HidesMembers annotation only has effect for members with these names
val HIDES_MEMBERS_NAME_LIST = setOf(Name.identifier("forEach"), Name.identifier("addSuppressed"))

意思是只有标识名是 forEachaddSuppressed 的成员才能用这个注解,是白名单制!

PS:强行覆盖的初衷是好的,但是偶尔也会带来一些副作用,比如 KT-39091 这是来自某开发者的吐槽。

@InlineOnly

package kotlin.internal

@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.BINARY)
internal annotation class InlineOnly

Specifies that this function should not be called directly without inlining.

kotlin.internal.InlineOnly 这个注解只能给内联函数(或者内联属性)用,用在其他地方是没有效果的。

这个注解是希望调用内联函数必须能够内联,它的具体效果是将其所标记的内联函数的可见性修饰符在编译时改成 private(当然编译后也是 private)。

// 编译前
@InlineOnly
inline fun someFun9() {}
// 编译后
@InlineOnly
private inline fun someFun9() {}
// 不要纠结细节,意思意思就可以了

这样可以阻止来自 Java 方面的调用。因为用 Java 调用 Kotlin 的内联方法并不会有内联效果。

本文完

我觉得剩下的那几个不用讲了。

啊,还有个名字很长的可以稍微提一下。

@Suppress("LATEINIT_INTRINSIC_CALL_ON_NON_LATEINIT")
fun main(args: Array<String>) {
   println(::fakeLateInit.isInitialized)
}

@Suppress("MUST_BE_INITIALIZED")
var fakeLateInit: String
   get() = throw Exception(field)

main 函数上面的注解删掉就能看到那个名字很长的注解的用处了。

顺便讲一下那三个 magic inference annotations 的八卦。几年前 JB 员工就说过会在未来哪天将这几个注解转正成 public 的,但是直到现在还是老样子。那 JB 员工这几年都在干什么?都在修这些注解的 bug 呀。现在用 1.3.72,把 New Inference 一打开,就能看到 @Exact 的 bug。另外他们还在考虑给 @OnlyInputTypes 换一个更好的名字,以及更好的编译错误信息(逃

本文真的完了。

知识共享许可协议
本作品采用知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请注明出处。

本文链接:https://aisia.moe/2020/07/16/kotlin-internal-annotations/