Kotlin提供了一个reified
关键字,这个关键字在实际项目中有很大的用处,比如泛型相关的封装,SDK封装等。今天我们就来学习下这块的知识。首先需要先了解下内联函数,也就是inline
关键字
内联函数(inline)
在Kotlin中内联函数就是用关键字inline
修饰的函数,它有什么作用呢?我看下面一段代码
fun main() {
getUserInfo {
println("get user info.")
}
}
inline fun getUserInfo(block: () -> Unit): String {
println("start to invoke getUserInfo method.")
block.invoke()
return "UserInfo"
}
很简单的一段代码,编译成apk后,我们可以看下它对应Class文件,代码如下:
public final class Main {
public static final void main() {
// 调用处,不在是调用函数,而是代码块
System.out.println((Object) "start to invoke getUserInfo method.");
System.out.println((Object) "get user info.");
}
public static final String getUserInfo(Functions<Unit> block) {
Intrinsics.checkNotNullParameter(block, "block");
System.out.println((Object) "start to invoke getUserInfo method.");
block.invoke();
return "UserInfo";
}
}
从上面的反编译的代码可以看到:
- 在调用内联函数的地方,不再是一个函数,而是函数内的代码块
- 而原来单独的
getUserInfo
函数还是存在,且函数参数编译成了对应的FunctionN
对象
上从面可以知道,内联函数在被调用时,并不是分配栈调用函数,而是在编译时,把内联函数中的代码块替换到调用处。
优点
为什么会引入inline的概念呢?主要的原因我个人认为有两个:
- 使用
inline
特性,性能上会有所提升,因为减少了调用函数时的内存,函数参数对象的创建等 - 因为Kotlin支持高阶函数,存在大量的匿名函数,
lambda
,使用inline
可以减少中间类的创建
注意:如果内联函数的代码块很多,那么有可能导致调用处的函数代码量很大。
reified
说这个之前,我先看下在Java中,如何实例化一个泛型对象?我们唯一的办法是像下面这样:
public static <T> T newInstance(Class<T> tClass) {
try {
return tClass.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
只能通过传递Class
的方式来创建,是因为存在泛型擦除的机制,无法知道T
到底是什么类型,无法获取泛型T
的Class
,所以只能在调用处,通过参数传递进来。
在Kotin中,同样也存在泛型擦除,但是他为了解决这个问题,新增了一个 reified
关键字,这个关键字的主要作用就是可以具体化泛型T
的类型。
我们现在用reified
关键字实现一个实例化泛型对象的工具方法,如下:
inline fun <reified T> newInstance(): T{
runCatching {
// 这里直接可以拿到T的class
val clazz = T::class.java
return clazz.newInstance()
}
return null
}
可以看到,我们不用像Java一样传递泛型的具体Class
,直接可以通过泛型拿到Class。
因为类型已知,所以正常的操作符如 !is
和 as
现在都能正常使用。那之前的我们封装SharedPreferences
保存key-value的那种方式都可以重新利用这个特性进行封装。
注意:
reified
参数只能用在内联函数中
原理
那为什么会有这个效果呢?我们先看一个例子:
fun main() {
getUserInfo("hello refied")
}
inline fun <reified T> getUserInfo(t: T) {
// 打印泛型的值
println("start to invoke getUserInfo method.$t")
}
上面是是一段很简单的代码,就是打印泛型t的值。我们反编译最后生成的class类,结果如下:
public final class Main {
public static final void main() {
System.out.println((Object) ("start to invoke getUserInfo method." + ((Object) "hello refied")));
}
}
从反编译的结果可以看出,调用的方法没有了,反而只有getUserInfo
方法内的代码块,而日志中的T
,直接替换成了调用方法传递进去的值hello refied
。
从这可以明白为什么reified要和内联函数一期使用,主要原因就是编译器在编译的时候,会把内联函数的代码替换到调用处。而reified
关键字修饰的泛型T
,会泛型T直接替换为调用处实际的数据类型。