Kotlin 是一种神奇的语言, 不是吗? 它充分利用了 Java 的巨大潜力, 但又以一种简单, 习以为常的方式将其展现出来, 并包含了各种功能. 不过, 尽管 Kotlin 有这么多优点, 但由于其功能繁多, 学习曲线也很陡峭.
对于某些人来说, 内联似乎很复杂, 而对于另一些人来说, 通过像 Koin 这样充分利用其潜力的库, 内联就像魔法一样. 但实际上, 一切都有解释, 而且可能比你想象的还要简单.
内联
那么, 什么是内联?
内联是一种特殊的操作符, 它允许我们将高阶函数或泛型属性中的代码转移到由 Kotlin 生成的Java bytecode级别的函数调用点.
这将在代码执行过程中为我们带来性能上的优势, 因为代码将被转移到使用点.
不明白? 让我们来看一个实际例子.
高阶函数
高阶函数(HOF) 在 Kotlin 和其他语言中处理回调问题方面享有盛誉. 在日常场景中, 我们经常会遇到这样的情况: 执行范围被转移, 而另一个线程(Thread)或协程(Coroutine)的返回值最终会在其操作结束时被看到.
但是回到这个示例, 让我们看看下面的代码:
fun main() {
val description = "Inline giving you magical powers"
executeOperation {
print(description)
}
}
fun executeOperation(operationTwo: () -> Unit) {
operationOne()
operationTwo()
operationThree()
}
请注意: 操作二返回到主函数的执行范围, 因此产生了一个简单的概念 回调. 在 Java 代码中, 我们会得到如下代码:
public static final void main() {
final String description = "Inline giving you magical powers";
executeOperation(new Function() {
@Override
public void invoke() {
System.out.print(description);
}
});
}
public static final void executeOperation(@NotNull Function operationTwo) {
operationOne();
operationTwo.invoke();
operationThree();
}
为了便于理解, bytecode生成的一些运算符将被省略.
现在神奇的事情发生了. 当我们在HOF函数的开头添加内联修饰符时, 除了修饰符之外, 没有任何视觉上的变化:
fun main() {
val description = "Inline giving you magical powers"
executeOperation {
print(description)
}
}
inline fun executeOperation(operationTwo: () -> Unit) {
operationOne()
operationTwo()
operationThree()
}
不过, 如果我们查看字节码生成的代码, 就会发现一些变化. 之前在 executeOperation 函数中的代码块已经完全转移到了主函数中:
public static final void main() {
String description = "Inline giving you magical powers";
operationOne();
System.out.print(description);
operationThree();
}
此外, Kotlin 还允许添加返回标签, 以中断当前作用域的执行流程:
fun main() {
val description = "Inline giving you magical powers"
executeOperation {
print(description)
return
}
}
现在, Kotlin 的作用域函数似乎不再那么神奇了吧? 哈哈
了解了内联函数的基本原理, 我们现在就可以开始研究它们的特殊修饰符了: noinline, crossinline, reified.
无内联 - noinline
想象一下, 现在你在同一个函数中使用多个 HOF, 但你需要每个函数都独立执行.
当我们需要将 HOF 作为参数传递给非内联函数, 或将其赋值给内联函数块中的变量时, 通常会出现这种情况.
请看下一段代码:
fun main() {
val description = "Inline giving you magical powers"
val descriptionInMainScope = "Inline in the main scope"
executeOperation({
print(description)
return // Allowed return
}, {
print(descriptionInMainScope)
return // Not allowed return
})
}
inline fun executeOperation(operationTwo: () -> Unit, noinline operationFour: () -> Unit) {
operationOne()
operationTwo()
operationThree()
operationFour()
}
此时, Java 的做法与不内联 HOF 的示例大致相同. 因此, 在局部代码块中, 我们失去了 return 的作用, 因为我们不知道函数何时会中断流程.
交叉内联 - Crossinline
Crossinline 基本上是用来表示 HOF 不允许在执行块中进行本地返回.
当你的 HOF 被另一个非内联函数, 功能接口 或来自 Java 代码的SAM(Single Abstract Method, 单一抽象方法)执行时, 通常会出现这种情况.
fun main() {
val description = "Inline giving you magical powers"
executeOperation {
print(description)
return // Not allowed return
}
}
inline fun executeOperation(crossinline operationTwo: () -> Unit) {
operationOne {
operationTwo()
}
}
fun operationOne(block: () -> Unit) {
block()
}
crossinline 和 noinline 都会根据构建的流程被生成脚本进行相当大的修改, 因此代码生成器可能会根据版本的不同改变在 Java 中的实现方式. 可能会出现新的类或中间方法, 但原则保持不变.
具化 - reified
从现在开始, 我们将拥有更多“魔法”. 将 Kotlin 隐式类型的威力发挥到极致!
我最喜欢并怀念的同类语言特性之一就是Reflection.
Reified 的意思是 “将抽象的东西看成具体的”. 特别是在 Java 中, 泛型是在非常特定的上下文中定义的, 所有泛型在实现时无一例外都是抽象的.
需要注意的一点是, 由于 Java 的编译和解释方式, 我们在 Java 中无法直接使用这一选项. 因此, 只要我们使用的函数在某一点上引用了对象中的泛型, 那么当我们试图在运行时检索该定义时, 就会遇到一个名为类型擦除的特殊属性.
对于那些好奇的人, 我留下了讨论类型擦除的官方文档.
了解了这一特殊性, 我们继续看代码.
看看下面代码中没有 inline 属性的泛型:
fun main() {
println(filterString(String::class.java))
println(requireMethods(String::class.java))
}
fun <T : Any> requireMethods(clazz: Class<T>): List<String> {
return clazz::class.java.methods.map { it.toString() }
}
fun <T : Any> filterString(clazz: Class<T>): T? {
itens().forEach {
if (clazz.isInstance(it)) {
@Suppress("UNCHECKED_CAST")
return it as T
}
}
return null
}
fun itens() = listOf<Any>(0, "Reified", 1f)
我们的 String 类型遇到了两种截然不同的情况, 它们都使用反射来映射其方法, 并在一个简单的列表中过滤它们.
你可能会在代码中感觉到各种不适: :class.java 块中的重复声明, 使用反射 API 对实例进行验证, 以及 UNCHECKED_CAST, 这表明由于 类型擦除, 我们不知道cast中的确切类型.
这就是伟大的洞察力: Reified! 当我们应用inline时, 我们已经知道代码是通过bytecode移动的, 正因为如此, 我们也能以一种具体的方式获得通用类型, 因为它将被直接引用.
请看我们在方法调用中具体了解其抽象类型后的代码:
fun main() {
println(filterString<String>())
println(requireMethods<String>())
}
inline fun <reified T : Any> requireMethods(): List<String> {
return T::class.java.methods.map { it.toString() }
}
inline fun <reified T : Any> filterString(): T? {
itens().forEach {
if (it is T) {
return it
}
}
return null
}
fun itens() = listOf<Any>(0, "Reified", 1f)
除了更加简洁之外, 我们还设法消除了上述所有不便之处...但是, 等等, 他是如何做到这一切的呢? Java 的最终结果是什么?
简而言之, 我将展示 requireMethods 和 filterString 的部分代码, 以及它们向主代码块的转移. 再次隐藏部分生成过程, 并改进生成变量的名称, 使其更易于理解:
public static final void main() {
// Function to acquire methods
Method[] methods = String.class.getMethods();
// Validation of the String in the list
if (element instanceof String) {
// Assignments
}
/**
* Codes related to the map and foreach methods, which are also inline. o/
* They will appear according to the order of method calls, but it's just a detail that can be ignored in the example.
*/
}
由于所有东西实际上都是 0 和 1, 所有的神奇之处都在于通过调用 Reflection API 的方法将类型转移到使用位置, 或者只是将其转移到验证位置. 因此, 最终我们真正将抽象的东西视为具体的东西, 或者具体化为: reified.
这一特性的应用远不止于我们使用带有泛型类型和DelegatedProperties的扩展.
限制
需要指出的一点是, 在应用程序接口或库中使用内联发布任何内容时, 你的代码最好在整个函数上下文中都是公开的. 不过, 为了防止代码被公开并干扰open/closed (OCP) 开闭原则. Kotlin 允许将代码内部化, 只要在方法或类中应用特殊注解 PublishedAPI 即可.
例如:
inline fun executeOperation(crossinline operationTwo: () -> Unit) {
operationOne {
operationTwo()
}
}
@PublishedAPI
internal fun operationOne(block: () -> Unit) {
block()
}
Kotlin 2.0.0 中的内联
第二个版本中的 Kotlin 实现了一些改进. 内联修改器将更加强大. 如果你从未学习过 callsInPlace, 那么现在正是向你展示这一常见契约的好时机.
DSL 中的 callsInPlace 方法负责向编译器报告 HOF 指令. 有些规则会在代码编译时应用!
例如, 当你需要在一个 var 或 val 中赋值时, 编译器会验证你要做的代码块. 在 HOF 的情况下, 你可能会遇到这些问题, 因为软件并不了解你执行的上下文. 本方法提供了一种强制识别并允许赋值的方法.
总结一下
内联函数已经帮上了大忙, 了解它们是优化日常工作和学习的基本步骤.
谁能想到像Koin这样的库会如此出名, 因为它们的这些特性可以简单地创建理想的复杂作用域.