- 原文地址:Exploring Kotlin’s hidden costs — Part 2
- 原文做者:Christophe B.
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:Feximin
- 校对者:PhxNirvana 、tanglie
本文是正在进行中的 Kotlin 编程语言系列的第二部分。若是你还未读过第一部分的话,别忘了去看一下。html
让咱们从新看一下 Kotlin 的本质,去发现更多 Kotlin 特性的实现细节。前端
有一种函数咱们在第一篇文章没有讲到:使用常规语法在其余函数内部声明的函数。这是局部函数,它们能够访问外部函数的做用域。java
fun someMath(a: Int): Int {
fun sumSquare(b: Int) = (a + b) * (a + b)
return sumSquare(1) + sumSquare(2)
}复制代码
让咱们先来谈谈他们最大的局限性:局部函数不能被声明为内联
(还不能?)而且一个包含局部函数的函数也不能被声明为内联
。尚未一个神奇的方法能够避免在这种状况下函数调用的成本。react
局部函数在编译后被转换为 Function
对象,就像 lambdas 那样,而且有着和上篇文章中描述的关于非内联函数的大多数相同的限制。编译以后的 Java 代码形式是这样的:android
public static final int someMath(final int a) {
Function1 sumSquare$ = new Function1(1) {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
return Integer.valueOf(this.invoke(((Number)var1).intValue()));
}
public final int invoke(int b) {
return (a + b) * (a + b);
}
};
return sumSquare$.invoke(1) + sumSquare$.invoke(2);
}复制代码
可是与 lambdas 相比有一个小的性能损失:因为调用者是知道这个函数的真正实例的,它的特定方法将被直接调用,而不是调用来自 Function
接口的通用合成方法。这意味着当从外部函数调用局部函数的时候不会有强制类型转换或者基础类型装箱现象发生。咱们能够经过查看字节码来验证这一点:ios
ALOAD 1
ICONST_1
INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I
ALOAD 1
ICONST_2
INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I
IADD
IRETURN复制代码
咱们能够看到那个被调用了两次的方法就是那个接收一个 int
参数而且返回一个 int
的方法,那个加法被当即执行而且没有任何中间的拆箱操做。git
固然,在每次方法调用的过程当中仍然有着建立一个新 Function
对象的成本。这个成本能够经过将局部函数重写为非捕获性的来避免:github
fun someMath(a: Int): Int {
fun sumSquare(a: Int, b: Int) = (a + b) * (a + b)
return sumSquare(a, 1) + sumSquare(a, 2)
}复制代码
如今这个相同的 Function
实例将被复用,仍然没有强制类型转换或者装箱状况发生。与典型的私有函数相比,局部函数惟一的缺点就是会额外生成一个有几个方法的的类。编程
局部函数是私有函数的一种替代,其优势是能够访问外部函数的局部变量。可是这些优势附带着隐性成本,那就是每次调用外部函数时都会生成一个
Function
对象,因此最好用非捕获性的函数。后端
Kotlin 语言中最好的特性之一就是明确区分了可空与不可空类型。这可使编译器在运行时经过禁止任何代码将 null
或者可空值分配给不可空变量来有效地阻止意想不到的 NullPointerException
。
让咱们声明一个公共的接收一个不可空 String
作为参数的函数:
fun sayHello(who: String) {
println("Hello $who")
}复制代码
如今看一下编译以后的等同的 Java 形式:
public static final void sayHello(@NotNull String who) {
Intrinsics.checkParameterIsNotNull(who, "who");
String var1 = "Hello " + who;
System.out.println(var1);
}复制代码
注意,Kotlin 编译器是 Java 的好公民,它在参数上添加了一个 @NotNull
注解,所以当一个 null
值传过来的时候 Java 工具能够据此来显示一个警告。
可是一个注解还不足以让外部调用实现空值安全。这就是为何编译器在函数的刚开始处还添加了一个能够检测参数而且若是参数为 null
就抛出 IllegalArgumentException
的静态方法调用。为了使不安全的调用代码更容易修复,这个函数在早期就会失败而不是在后期随机地抛出 NullPointerException
。
在实践中,每个公共的函数都会在每个不可空引用参数上添加一个 Intrinsics.checkParameterIsNotNull()
静态调用。私有函数不会有这些检查,由于编译器会保证 Kotlin 类中的代码是空值安全的。
这些静态调用对性能的影响能够忽略不计而且他们在调试或者测试一个 app 时确实颇有用。话虽这么说,但你仍是可能将他们视为一种正式版本中没必要要的额外成本。在这种状况下,能够经过使用编译器选项中的 -Xno-param-assertions
或者添加如下的混淆规则来禁用运行时空值检查:
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}复制代码
注意,这条混淆规则只有在优化功能开启的时候有效。优化功能在默认的安卓混淆配置中是禁用的。
虽然显而易见,但仍需谨记:可空类型都是引用类型。将基础类型变量声明为 可空的话,会阻止 Kotlin 使用 Java 中相似 int
或者 float
那样的基础类型,相应的相似 Integer
或者 Float
那样的装箱引用类型会被使用,这就引发了额外的装箱或拆箱成本。
与 Java 中容许草率地使用与 int
变量几乎彻底同样的 Integer
变量相反,因为自动装箱和不须要考虑空值安全的缘由,在使用可空类型时 Kotlin 会迫使你编写安全的代码,所以使用不可空类型的好处变得愈来愈清晰:
fun add(a: Int, b: Int): Int {
return a + b
}
fun add(a: Int?, b: Int?): Int {
return (a ?: 0) + (b ?: 0)
}复制代码
为了更好的可读性和更佳的性能尽可能使用不可空基础类型。
Kotlin 中有三种数组类型:
IntArray
, FloatArray
还有其余的:基础类型数组。编译为 int[]
, float[]
和其余的类型。Array<T>
:不可空对象引用类型化数组,这涉及到对基础类型的装箱。Array<T?>
:可空对象引用类型化数组。很明显,这也涉及到基础类型的装箱。若是你须要一个不可空的基础类型数组,最好用
IntArray
而不是Array<Int>
来避免装箱(操做)。
Kotlin 容许声明具备数量可变的参数的函数,就像 Java 那样。声明语法有点不同:
fun printDouble(vararg values: Int) {
values.forEach { println(it * 2) }
}复制代码
就像 Java 中那样,vararg
参数实际上被编译为一个给定类型的 数组 参数。你能够用三种不一样的方式来调用这些函数:
printDouble(1, 2, 3)复制代码
Kotlin 编译器会将这行代码转化为建立并初始化一个新的数组,和 Java 编译器作的彻底同样:
printDouble(new int[]{1, 2, 3});复制代码
所以有建立一个新数组的开销,但与 Java 相比这并非什么新鲜事。
这就是不一样之处。在 Java 中,你能够直接传入一个现有的数组引用做为可变参数。可是在 Kotlin 中你须要使用 分布操做符:
val values = intArrayOf(1, 2, 3)
printDouble(*values)复制代码
在 Java 中,数组引用被“原样”传入函数,而无需分配额外的数组内存。然而,分布操做符编译的方式不一样,正如你在(等同的)Java 代码中看到的:
int[] values = new int[]{1, 2, 3};
printDouble(Arrays.copyOf(values, values.length));复制代码
每当调用这个函数时,如今的数组总会被复制。好处是代码更安全:容许函数在不影响调用者代码的状况下修改这个数组。可是会分配额外的内存。
注意,在 Kotlin 代码中调用一个有可变参数的 Java 方法会产生相同的效果。
分布操做符主要的好处是,它还容许在同一个调用中数组参数和其余参数混合在一块儿进行传递。
val values = intArrayOf(1, 2, 3)
printDouble(0, *values, 42)复制代码
这是如何编译的呢?生成的代码十分有意思:
int[] values = new int[]{1, 2, 3};
IntSpreadBuilder var10000 = new IntSpreadBuilder(3);
var10000.add(0);
var10000.addSpread(values);
var10000.add(42);
printDouble(var10000.toArray());复制代码
除了建立新数组外,一个临时的 builder 对象被用来计算最终的数组大小并填充它。这就使得这个方法调用又增长了另外一个小的成本。
在 Kotlin 中调用一个具备可变参数的函数时会增长建立一个新临时数组的成本,即便是使用已有数组的值。对方法被反复调用的性能关键性的代码来讲,考虑添加一个以真正的数组而不是
可变数组
为参数的方法。
感谢阅读,若是你喜欢的话请分享本文。
继续阅读第三部分:委派属性和范围。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。