翻译说明:
原标题: Getting Real with Kotlin's Reified Type Parameters
原文地址: typealias.com/guides/gett…
原文作者: Dave Leeds
历史背景:
我们都知道Java中的泛型是在JDK1.5的版本引入的,可是集合Collection在JDK1.2版本中就引入的,我们现在所看到的List<T>
,是在泛型出来后加入的,那么JDK1.2之前就直接用List
(java中俗称原生态类型)表示。问题来了为了兼容之前的版本Java采用所谓的伪泛型,伪泛型有个什么特征我想大家应该猜到那就是泛型擦除,就是泛型类型信息在编译期都会被抹掉,不管你是List<String>
还是List<Float>
在运行时他们都一样,那都是List类型,泛型类型信息已经被擦除了。当然泛型擦除也有它的好处,当然这不是这次讨论的重点。伪泛型对应的就是真泛型,如果熟悉C#就知道,它是真泛型,不会存在类型擦除情况,具体可以自己去了解下。
时势造英雄Kotlin登场:
显然我们知道,泛型擦除在一些开发场景下是有很大影响,使用起来非常不便,Kotlin这门新的语言不像Java一样有太多的历史负担,它就像是全局者一样,看着Java之前的坑走过来的,于是乎它想来填一填。有的人可能会问了那它是不是像C#那样采用真泛型,答案不是。我们都知道Kotlin力求做到与Java百分百的互操作性,所以Kotlin妥协了还是采用伪泛型,所以它和Java一样依然会存在泛型擦除问题。但是很幸运地是Kotlin偷偷给我们开了一个后门那就是今天的主角Reified实化类型参数,它可以保证运行时依然能拿到泛型具体实际类型。Reified实化类型参数已经大量运用在Kotlin的anko中,关于这块后面博客会细讲。
进入正题(开始翻译)
让我们来想一想在Kotlin中你可以类名来做些什么-想想你在源码中编写类名的所有场景。我想到了列举以下15种情景,没想全可能会有漏一些。让我们一起来看下吧...
- 1、定义一个成员属性
private val thing: Thing
- 2、函数参数
fun doSomething(thing: Thing) {}
- 3、类型实参
val list = listOf<Thing>()
- 4、类型形参约束
class Item<T : Thing>
- 5、强制类型转换
something as Thing
- 6、定义一个类
open class Thing {}
- 7、继承一个类
class Other : Thing()
- 8、导入一个类
import com.example.Thing
- 9、给类名定义typealias别名
typealias Thingy = Thing
- 10、构建一个对象
val thing = Thing()
- 11、捕获一个错误类型
catch (e: ExceptionalThing)
- 12、调用静态方法
Thing.doStaticStuff()
- 13、定义函数引用
val function = Thing::doSomething
- 14、类型比较
something is Thing
- 15、获得类的一个Class对象:
val clazz = Thing::class.java
大问题来了
现在,最大的问题是:
在上面哪些案例中我们应该使用泛型类型参数引用而不是真实的类名?
换句话说,上面15种案例中哪些我们可以用类型参数比如 T
去替代Thing
类呢?
你在哪些地方可以引用类型参数?
你想到了什么? 这是我能想到的 - 上面的案例1-5都可以采用类型参数。让我们一起展示所有五个:
-
class GenericThing<T>(constructorArg: T) {
-
// 1. Define a member property 定义一个成员属性
-
private val thing: T = constructorArg
-
// 2. Define the type of a function argument 定义函数参数
-
fun doSomething(thing: T) = println(thing)
-
// 3. Use as a type argument 定义泛型类型实参
-
fun emptyList() = listOf<T>()
-
// 4. Use as a type parameter constraint, and... 使用作为类型形参约束
-
// 5. Cast to the type (produces "unchecked cast" warning) 强制类型转换
-
fun <U : T> castIt(): T = thing as U
-
}
你在哪些地方可以不引用类型参数?
引用类型参数适用于案例1-5。但是对于案例6-15,如果我们要在(例如,Thing或ExceptionalThing)的地方替换类型参数(例如T),则最终会出现编译器错误。
- 在某些情况下,使用类型参数是没有意义的。例如,在案例6中我们正在定义一个类 - 根据其类型参数之一定义一个类有什么意义呢?
- 在其他情况下,Java和Kotlin通过反射提供一些变通的方法,例如构建对象(案例10) 但在某些情况下,能够使用类型参数肯定会很好。特别是,案例14和15--比较类型和分配类对象 - 在某些情况下会非常方便。
Reified类型实化参数介绍
Java限制了哪些类型是reifiable - reifiable也就意味着它们“在运行时完全可用”(具体可查阅:Java SE specs on reifiable types),泛型类型参数通常在编译期间被擦除,但是在Kotlin中的reified类型参数的情况下, 由于底层语法下一些巧妙的技巧,让运行时也能准确拿到泛型参数类型信息。
Reified类型参数仅适用于函数(或具有get()函数的扩展属性),并且仅适用于声明为inline内联的函数。这是一个例子:
inline fun <reified T> Any.isInstanceOf(): Boolean = this is T
当您将函数标记为inline时,编译器会把实现内联函数的字节码插入到每次调用发生的地方。这就是reified类型的工作原理 - 具体实际类型在调用地方是已经知道的,因此在调用x.isInstanceOf<String>()
有效地把x编译为String.
reified类型实化参数经常被使用到的几个地方
上面的案例15是许多Kotlin开发人员最喜欢的案例。假设我们有一个User类,以及我们想要读取的JSON字符串
-
data class User(val first: String, val last: String)
-
val json = """{
-
"first": "Sherlock",
-
"last": "Holmes"
-
}""
在Java序列化库(如Gson)中,当您想要反序列化该JSON字符串时,您最终必须将Class对象作为参数传递,以便Gson知道您想要的类型。
User user = new Gson().fromJson(getJson(), User.class);
现在,让我们一起展示reified类型实化参数的魔法 我们将创建一个非常轻量级的扩展函数来包装Gson方法:
-
inline fun <reified T> Gson.fromJson(json: String) =
-
fromJson(json, T::class.java)
现在,在我们的Kotlin代码中,我们可以反序列化JSON字符串,甚至根本不需要传递类型信息!
val user: User = Gson().fromJson(json)
Kotlin根据它的用法推断出类型 - 因为我们将它分配给User
类型的变量,Kotlin使用它作为fromJson()
的类型参数。或者,您可以使类型推断其他的类:
val user = Gson().fromJson<User>(json)
在这种情况下,从传递给fromJson()
的类型参数推断出user
类型。