Kotlin的泛型:类型擦除与实化

JVM 的泛型一般是通过类型擦除实现的,就是说泛型的类型实参在运行时不保留。

和 Java 一样,Kotlin 的泛型在运行时也被擦除了,但是 Kotlin 可以通过将函数声明为 inline 来解决这个问题。Kotlin 可以声明一个 inline 函数,使用实化 reified 使其类型实参不被擦除。

类型检查与转换

在运行时,List 的类型实参 String 被擦除了,只能知道它是一个 List,不能知道它是否声明为一个字符串列表。

list1 和 list2 在运行时每个都是 List,不能看出它们是否声明为字符串或者整数列表。

fun erase() {
    val list1: List<String> = listOf("a", "b");
    val list2: List<Int> = listOf(1, 2, 3)
}

类型检查

因为类型实参被擦除,不能检查带类型实参的泛型。

下面的代码不会编译,因为运行时 List 的类型实参被擦除。

fun <T> checkInstance(value: List<T>) {
    // 编译器报错Cannot check for instance of erased type: List<String>
    if (value is List<String>) {
        println("is string list.")
    }
}

下面的代码可以编译过,用来检查一个值是否是列表,而不是 set 或者其他对象。* 是星号投影,表示未知类型实参的泛型类型,相当于 Java 的 List<?>。

fun <T> checkListInstance(value: List<T>) {
    if (value is List<*>) {
        println("is a list with any type")
    }
}

类型转换

可以使用 as 或者 as? 转换泛型类型。

下面的代码可以编译,只是报警告:Unchecked cast: Collection<*> to List。未经过检查的类型转换。

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected");
    println(intList.sum())
}

当 c 可以转换成 List 时,打印列表元素之和,否则抛出 IllegalArgumentException。

传入 List 时,正常返回列表元素的和。

fun main() {
    // 6
    printSum(listOf(1, 2, 3))
}

传入 Set 时,擦除后的泛型类型 Set 无法转换为 List,抛出 IllegalArgumentException。


fun main() {
	// Exception in thread "main" java.lang.IllegalArgumentException: List is expected
    printSum(setOf(1, 2, 3))
}

传入 List 时,能转换成功,但是 sum() 报错,抛出 ClassCastException,因为 Kotin 试图将元素转化为 Int 然后 sum,但是 String 无法转换为 Int。

fun main() {
    // Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
    printSum(listOf("a", "b", "c"))
}

已知类型实参的转换

Kotlin 对于编译期间已知的类型信息,做 is 检查是允许的。

下面的代码编译器不会报错也不会报警告,因为参数的类型实参是已知的,为 Int。

fun printSumInt(c: Collection<Int>) {
    if (c is List<Int>) {
        println(c.sum())
    }
}

fun main() {
    // 6
    printSumInt(listOf(1, 2, 3))
}

带实化类型的函数

泛型函数的类型参数在运行时也会被擦除。下面的方法无法编译,因为类型参数被擦除,运行时不知道类型参数 T 的具体类型,无法检查类型参数 T 的类型。

// Cannot check for instance of erased type: T
// Make type parameter reified and function inline
fun <T> isA(value: Any) = value is T

但是编译器也给出了提示 Make type parameter reified and function inline,将类型参数实化并且变为内联函数。

inline 和 reified

下面的方法可以编译通过,正常运行。区别在于函数变为内联函数 inline,类型参数变为实化参数 reified。

inline fun <reified T> isA(value: Any) = value is T

fun printIsA() {
    // true
    println(isA<String>("abc"))
    // false
    println(isA<String>(123))
}

filterIsInstance 函数

filterIsInstance 是标准库中的一个内联实化函数。它会过滤列表中所有类型为指定类型参数的元素。这里是 String 类型。

fun filterIsInstance() {
    val items = listOf("one", 2, "three")
    // [one, three]
    println(items.filterIsInstance<String>())
}

查看 filterIsInstance 的源码实现,可以看出它是一个内联函数 inline fun,同时泛型类型参数 R 被实化 reified,因此不会擦除类型参数。

因此在函数内部可以使用 element is R 类型检查,只有类型为 R 的元素才会被加入到列表。

public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
    return filterIsInstanceTo(ArrayList<R>())
}

public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C {
    for (element in this) if (element is R) destination.add(element)
    return destination
}

类型实化后 Kotlin 生成的字节码和以下代码类似。类型实参 String 替换了类型参数 T,因此可以使用 element is String 类型检查。

fun afterReified() {
    val items = listOf("one", 2, "three")
    val destination = ArrayList<String>()
    
    for (element in items) {
        if (element is String) {
            destination.add(element)
        }
    }
    
    // [one, three]
    println(destination)
}

实化类型代替类引用

实化类型的一个常用场景是封装 Java.lang.Class 类的引用。

loadService 函数

下面的代码使用 ServiceLoader 加载 Service::class.java,返回一个 serviceImpl 实现类。Service::class.java 是 Kotlin 的 Service 类在 Java 中的对应类。

loadService 是使用内类函数和实化类型参数的改造函数。可以看出 loadService 的语法跟简单,只需要传入类型实参到 即可。

fun reifiedClass() {
    val serviceImpl = ServiceLoader.load(Service::class.java)
    // reified
    val serviceImpl2 = loadService<Service>()
}

inline fun <reified T> loadService() {
    ServiceLoader.load(T::class.java)
}

interface Service {
    fun work()
}

简化 startActivity

在 Android 开发中使用 reified 实化类引用的场景是启动 Activity。

下面的代码给 Intent 传入了 SampleActivity::class.java,然后启动 SampleActivity。

fun startActivity(context: Context) {
    val intent = Intent(context, SampleActivity::class.java)
    context.startActivity(intent)
}

使用 reified 实化需要启动的 Activity 类,并将它作为 Context 接收者的扩展函数。

inline fun <reified T> Context.startActivity() {
    val intent = Intent(this, T::class.java)
    startActivity(intent)
}

可以进一步封装,指定类型参数的泛型上界为 Activity,可选参数 Bundle。

inline fun <reified T : Activity> Activity.startActivity(bundle: Bundle? = null) {
    val intent = Intent(this, T::class.java)
    bundle?.let {
        intent.putExtras(it)
    }
    startActivity(intent)
}

简化后只需要调用 startActivity(),指定类型实参为 SampleActivity。

         startActivity<SampleActivity>()

实化类型的限制

实化类型参数很方便,但是它也有一些限制。

可以使用实化类型参数的几个方面:

  • 类型检查和类型转换 is, !is, as, as?
  • Kotlin 反射 API,::class
  • 获取 Java.lang.Class,::class.java
  • 作为类型参数用来调用其他函数

不能使用实化类型参数的几个方面:

  • 创建类型参数指定的类实例
  • 调用类型参数的伴生对象的方法
  • 调用实化类型参数的函数时,使用非实化类型参数作为类型参数
  • 将类、属性、或者非内联函数的类型参数变为实化类型

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值