经过前面的学习,对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。
参考资料
- Type erasure
- 6. Generics at Runtime
- How to Convert a Type-Erased List to an Array in Kotlin
- Discussion about Type Erasure
- How does erasure work in Kotlin?
- Reified Generics in Kotlin
- Type erasure and reified in Kotlin
欢迎查找并关注 大众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」,「点赞」,「在看」,「收藏」,「分享」 总要有一个吧!