Kotlin 中的内联函数(Inline Function)是一种特殊的函数,与普通函数不同, 编译器会在编译时将内联函数中的代码插入到调用处, 避免函数调用的开销。它的作用和 C++ 中的内联函数类似,都是为了提高代码的执行效率。
定义内联函数
内联函数 的定义和普通函数类似, 只是在函数名前添加 inline 关键字即可。
kotlin
inline fun inlineFunction(block: () -> Unit) {
println("Inline function")
block()
}
从语法上看,调用方式和普通函数相似
kotlin
fun main() {
inlineFunction {
println("Hello, World!")
}
}
以上代码与下面的代码在执行效果上完全一致
kotlin
fun main() {
println("Inline function")
println("Hello, World!")
}
内联函数的return
kotlin 的 lambda 表达式return必须使用 return@scope 来指定返回的作用域,由于内联函数的代码会被插入到调用处,所以内联函数可以使用 return 而不必须添加@限定符。比如
kotlin
fun main() {
inlineFunction {
println("Hello, World!")
return
}
}
而普通函数则必须使用 return@scope 来返回, 比如
kotlin
fun main() {
normalFunction {
println("Hello, World!")
return@normalFunction
}
}
为什么?因为内联函数的代码会被插入到调用处,所以上面的内联函数返回就相当于
kotlin
fun main() {
println("Inline function")
println("Hello, World!")
// 这里的return是可以不添加限定符直接返回的
return
}
Kotlin 中的 return
从上面对内联函数的描述中,我们可以思考一个问题:什么时候return不需要添加@限定符呢?
- 当内联函数的lambda参数在顶层函数中被调用时,此时不添加限定符,return 就相当于直接返回顶层函数。
- 匿名函数中的return, 也不需要添加@限定符
内联函数在非内联函数中调用的时候, 就需要使用@限定符来指定返回的作用域。比如
kotlin
fun main() {
normalFunction {
inlineFunction {
println("Hello, World!")
// 表示返回到inlineFunction
return@inlineFunction
// 或者返回到normalFunction
// return@normalFunction
}
}
inlineFunction {
println("Hello, World!")
// 直接返回到顶层函数,不需要添加@限定符
return
// 或者返回到normalFunction
// return@normalFunction
}
inlineFunction {
inlineFunction {
// 直接返回到顶层函数,可以不添加@限定符
return
// 或者返回到inlineFunction
// return@inlineFunction
}
}
normalFunction(fun ():Unit {
// 匿名函数中的return, 不需要添加@限定符
return
})
}
但是这里有一个问题,加了@限定符的return和不加@限定符的return有什么区别呢?
- 如果加了限定符,退出的只是当前作用域
- 而不加限定符,退出的是整个函数
Kotlin 中的 return 设计
那么,为什么Kotlin要设计成这样呢?为什么不设计的和Java一样,return返回的只是当前的作用域呢?目的有两个
- 增加比Java代码更大的灵活性
- 避免使用内联函数时的逻辑混淆
1. 增加比Java代码更大的灵活性
Java代码中,lambda表达式中的return只能返回当前lambda作用域,而Kotlin中的return可以返回任意作用域。这就增加了比Java代码更大的灵活性。
比如我们有一个需求,遍历一个列表,如果符合条件,终止遍历,我们看看在Java中怎么实现
java
public <T> void breakOnCondition(List<T> list, Predicate<T> predicate) {
for (T t : list) {
if (predicate.test(t)) {
return;
}
}
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
breakOnCondition(list, i -> i == 3);
}
那么,在Kotlin中我们甚至不需要这样设计。对于遍历场景,我们可以直接使用lambda表达式结合 return@scope 来终止当前遍历操作。
kotlin
fun main() {
val list = listOf(1, 2, 3, 4, 5)
list.forEach {
if (it == 3) {
// 直接返回forEach循环
return@forEach
}
}
}
2. 避免使用内联函数时的逻辑混淆
如果可以不带有@限定符,那么用户在编写代码的时候,很难确定程序的行为,比如下面的代码
kotlin
fun normalFunction(block: () -> Unit) = block()
inline fun inlineFunction(block: () -> Unit) = block()
fun main() {
normalFunction {
// 这里是无法编译通过的,我们假定可以通过
return
}
// 会被执行
println("After normalFunction")
inlineFunction {
return
}
// 不会被执行
println("After inlineFunction")
}
假设上面的代码正确,并且可以编译通过。
这里的 normalFunction 中的return,结束的是 normalFunction 函数。
而 inlineFunction 中的return,结束的却是main函数,因为inlineFunction 中的代码会被插入到调用处,所以inlineFunction 中的return 就相当于直接返回main函数。
那么用户在编写代码的时候,就很容易混淆。
而如果非内联函数的lambda参数要求只能使用return@scope来返回,就会避免这种容易混淆的情况。
kotlin
fun main() {
normalFunction {
return@normalFunction
}
// 会被执行
println("After normalFunction")
inlineFunction {
return@inlineFunction
}
// 会被执行
println("After inlineFunction")
inlineFunction {
return
}
// 不会被执行
println("After inlineFunction")
}
reified 类型参数
reified 类型参数是Kotlin中一种特殊的类型参数,它可以在运行时获取到类型信息。还记得Java中怎么获取泛型类型吗?
java
public <T> T parseObj(String json, Class<T> clazz) {
return new Gson().fromJson(json, clazz);
}
在Java中,我们必须添加 Class<T> clazz 参数来指定泛型类型。否则,我们无法在运行时获取到泛型类型。因为Java代码中如果没有和泛型相关的对象传入进来的话,是无法在运行时获取到泛型类型的。
但是在Kotlin的内联函数中,提供了reified 类型参数,它可以在函数不提供参数信息情况下,获取到泛型类型。比如下面的代码
kotlin
inline fun <reified T> parseObj(json: String): T {
return new Gson().fromJson(json, T::class.java)
}
想象一下这个参数可以给我们带来多大的便利性,我们可以直接在函数中使用T::class.java来获取到泛型类型,而不需要传入Class<T> clazz 参数。
MybatisPlus 提供了 KtQueryChainWrapper 类,通过这个类,可以通过lambda的方式构建查询参数。
kotlin
@Autowired
private lateinit var baseMapper: BaseMapper<Config>
val chain = KtQueryChainWrapper(baseMapper, entityClass=Config::class.java)
val config = chain.eq(Config::name, "test").one()
为了方便,我们可以为 BaseMapper 添加一个扩展方法,来获取 KtQueryChainWrapper 类。
kotlin
fun <T : Any> BaseMapper<T>.query(): KtQueryChainWrapper<T> {
// 很要命的是 mybatisplus 要求必须填写 entityClass 参数,否则会报错
return KtQueryChainWrapper(baseMapper = this, entityClass = entityClass())
}
// 使用的时候就会很方便
val config = baseMapper.query().eq(Config::name, "test").one()
但是,怎么获取 entityClass 呢?如果没有 reified 关键字, 我们只能通过反射来获取。比如下面的代码
kotlin
private fun <T : Any> BaseMapper<T>.entityClass(): Class<T> {
val searchEntityClass = javaClass.searchEntityClass<T>()
if (searchEntityClass != null) {
return searchEntityClass
}
throw IllegalArgumentException("无法获取实体类")
}
private fun <T> Class<*>.searchEntityClass(): Class<T>? {
for (type in genericInterfaces) {
if (type is ParameterizedType) {
if (type.rawType == BaseMapper::class.java) {
return type.actualTypeArguments[0] as Class<T>
}
} else {
if (type is Class<*>) {
return type.searchEntityClass()
}
}
}
return null
}
这时候如果我们使用 内联函数 + reified 类型参数,就可以避免使用反射来获取泛型类型。比如下面的代码
kotlin
inline fun <reified T : Any> BaseMapper<T>.query(): KtQueryChainWrapper<T> {
// 在使用了reified 类型参数后,就可以直接使用 T::class.java 来获取泛型类型
return KtQueryChainWrapper(baseMapper = this, entityClass = T::class.java)
}
可以看到,使用了 reified 类型参数后,就可以直接使用 T::class.java 来获取泛型类型,而不需要通过反射来获取。