大众号「稀有猿诉」 原文链接 Deep Dive into Kotlin Generics Runtime

经过前面的学习,对Kotlin的泛型现已有了比较全面的了解了,泛型的意图是让通用的代码更加的类型安全。现在我们离写出类型安全的泛型代码还差最后一块拼图,那便是泛型的类型擦除,今天就来深化地学习一下运行时的泛型,彻底的弄懂类型擦除的前因后果,并学会怎么在运行时做类型查看和类型转化,以期完成拼图掌握泛型,写出类型安全的通用代码。

深化研究Kotlin运行时的泛型

关于泛型论题的一系列文章:

泛型类型擦除(Type erasure)

泛型的类型安全性(包含类型查看type check,和类型转化type casting)都是由编译器在编译时做的,为了保持在JVM上的兼容性,编译器在确保完类型安全性后会对泛型类型进行擦除(Type erasure)。在运行时泛型类型的实例并不包含其类型信息,也便是说它不知道详细的类型参数,比方Foo<Bar>和Foo<Baz?>都被擦除成了Foo<*>,在虚拟机(JVM)来看,它们的类型是相同的。

由于泛型Foo<T>的类型参数T会被擦除(erased),所以与类型参数相关的类型操作(类型查看is T和类型转化as T)都是不答应的。

可行的类型查看和转化

尽管类型参数会被擦除,但并不是说对泛型彻底不能进行类型操作。

星号类型操作

由于一切泛型会被擦除成为星号无界通配Foo<*>,它相当于Foo<Any?>,是一切Foo泛型的基类,类型参数Any?是根基类,所以可以进行类型查看和类型转化:

if (something is List<*>) {
	something.forEach { println(it) } // 元素被视为Any?类型
}

针对星号通配做类型操作,类型参数会被视为Any?。但其实这种类型操作没有任何意义,究竟Any是根基类,任何类当成Any都是没有问题的。

彻底已知详细的类型参数时

别的一种状况便是,整个办法的上下文中现已彻底知道了详细的类型参数时,不触及泛型类型时,也是可以进行类型操作的,说的比较绕,我们来看一个:

fun handleStrings(list: MutableList<String) {
	if (list is ArrayList) {
		// list is smart-cast to ArrayList<String>
	}
}

这个办法并不触及泛型类型,现已知道了详细的类型参数是String,所以类型操作也是可行的,由于编译器知道详细的类型,能对类型进行查看 确保是类型安全的。并且由于详细类型参数String可以揣度出来,所以<String>是可以省掉的。

未查看的转化

当编译器能揣度出详细的类型时,进行类型转化便是安全的,这便是被查看的转型(checked cast),如上面的。

如果无法揣度出类型时,比方触及泛型类型T时,由于类型会被擦除,编译器不知道详细的类型,这时as T或许as List<T>都是不安全的,编译器会报错,这便是未查看转型(unchecked cast)。

但如果能坚信是类型转化是安全的,可以用注解@Suppress(“UNCHECKED_CAST”)来疏忽。

用要害reified润饰inline泛型函数

要想可以对泛型类型参数T做类型操作,只能是在用要害字reified润饰了的inline泛型函数,在这种函数体内可以对泛型类型参数T做类型操作,如:

inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
    if (first !is A || second !is B) return null
    return first as A to second as B
}
val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)
val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()

需求留意的是要害字reified可以让针对类型参数T的操作得到编译器的查看,确保安全,是答应的。可是对于泛型仍是不答应的,如:

inline fun <reified T> List<*>.asListOfType(): List<T>? =
    if (all { it is T })
        @Suppress("UNCHECKED_CAST")
        this as List<T> else
        null

这个inline泛型函数用要害字reified润饰了,因而针对类型参数T是答应类型查看类型转化,如第2行是答应的。但泛型仍是不合法,如第4行,这时可以用上一小节说到的注解@Suppress(“UNCHECKED_CAST”)来疏忽未查看类型转化。

inline和reified的原理

对于一些泛型工厂办法,就十分合适使用inline和reified,以确保转化为类型参数(由于工厂办法最终肯定要as T)是答应的且是安全的:

inline fun <reified T> logger(): Logger = LoggerFactory.getLogger(T::class.java)
class User {
    private val log = logger<User>()
    // ...
}

要害字reified其实也没有什么奥秘的,由于这是inline函数,这种函数是会把函数体嵌入到任何调用它的当地(call site),而每个调用泛型函数的当地必定会有明确的详细类型参数,那么编译器就知道了详细的类型能确保类型安全(checked cast)。上面的工厂办法在调用时就会大概变成酱紫:

class User {
	private val log = LoggerFactory.getLogger(User.class.java)
}

这时其实在函数体内现已知道了详细的类型参数User,编译器可以进行类型查看,所以是安全的。

总结

本文深化的讨论一下运行时泛型的一些特性,泛型类型在运行时会被擦除,无法做泛型相关的类型操作,由于编译器无法确保其类型安全。破例便是在用reified润饰的inline函数中可以对类型参数T做类型操作,但泛型类型(带尖括号的<T>)仍是会被擦除,可以用注解@Suppress(“UNCHECKED_CAST”)来疏忽unchecked cast。

参考资料

欢迎查找并关注 大众号「稀有猿诉」 获取更多的优质文章!

原创不易,「打赏」「点赞」「在看」「收藏」「分享」 总要有一个吧!