这是关于 Kotlin 编程语言的第 2 部分更新。 如果还没有读过第一部分,请点击超链接跳转阅读。
让我们继续发现更多 Kotlin 功能的实现细节。
第一篇文章中有一类函数我们并未谈及:在其它函数内部,使用正规语法声明的函数。这些函数称为局部函数,它们可以访问外部函数的作用域。
fun someMath(a: Int): Int { fun sumSquare(b: Int) = (a + b) * (a + b) return sumSquare(1) + sumSquare(2) }
我们先来说说它最大的局限:局部函数不能声明为内联(还没有?),而且包含局部函数的函数也不能声明为内联。没有任何方法可以避免这种情况下的函数调用开销。
局部函数编译后会被转换为函数对象,就像 Lambda 一样,基本和上一篇文章中提到的关于非内部函数受相同的限制。编译后代码的 Java 呈现像这样:
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); }
与 Lambda 相比这里存在一点性能损失:因为调用者知道这些函数的实例,调用的是它的具体方法而不是通用的函数接口方法。这意味着从外部函数调用局部函数时不会出现对基础类型进行装箱的操作。我们可以看看下面的字节码来验证这一说法:
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,而且它是立即执行,没有进行中间的拆箱操作。
当然每次调用都会产生创建新函数对象的代价。将本地函数重写为非捕获的可以防止这种事情发生。
fun someMath(a: Int): Int { fun sumSquare(a: Int, b: Int) = (a + b) * (a + b) return sumSquare(a, 1) + sumSquare(a, 2) }
现在函数实例可以复用,而且同样不会出现任何转换和装箱操作。与传统的私有函数相比,局部函数唯一的不足是会生成一个额外的,具有某些方法的类。
局部函数可代替私有函数,其优点在于可以访问外部函数的局部变量。这种好处会隐藏对每次调用产生函数对象的代价,因此,首选使用非捕获局部函数。
Kotlin 语言最好的特性之一就是明确区分了可空和非空类型。通过编译器禁止代码将 null 或可空值赋予非空类型变量,可以有效防止运行时意外的产生 NullPointerException。
来声明一个仅有函数,它接收一个非空字符串作为参数:
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 注解,这样 Java 工具可以在传入 null 值的时候根据这一注解给出警告。
不过注解还不足以保证外部调用的空安全。这就是为什么编译器会在一开始添加一个静态方法调用来检查参数,并在它为 null 提时候抛出 IllegalArgumentException。这样函数会及早而稳定失败,而不是在之后随机的由 NullPointerException 导致失败,这会使不安全的调用代码及早得到修复。
实践过程中,每个公共函数都会为每个非空引用的参数静态调用 Intrinsics.checkParameterIsNotNull()。这些检查并不会添加为私有函数,因为编译器可以保证 Kotlin 类中的那些代码是空安全的。
这些静态调用所产生的性能影响无足轻重,它们在调试和测试应用期间非常有用。也就是说,你可以把它们看作是非必要的额外开销。因此,使用 -Xno-param-assertionscompiler 选项或使用下面的 ProGuard 规则可以禁止运行时的空检查:
-assumenosideeffects class kotlin.jvm.internal.Intrinsics { static void checkParameterIsNotNull(java.lang.Object, java.lang.String); }
注意,ProGuard 规则只会在启用了优化的时候生效。而优化在默认的 Android ProGuard 配置中是禁用的。其它翻译版本 (1) 加载中
这似乎很明显,但需要提醒:可空类型始终是引用类型。 将基本类型的变量声明为可空,可以防止 Kotlin 使用像 int 或 float 这样的 Java 基本类型,而是将使用像 Integer 或 Float 这样的包装引用类型,这涉及额外的装箱和拆箱操作成本。
Java 允许您随意的使用一个 Integer 变量几乎完全像一个 int 变量,而与 Java 相反,由于自动装箱和忽略零安全性,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>:非 null 对象引用的数组。这涉及到原始类型的封箱(boxing)。
Array<T?>: 可以为空的对象引用的数组。这很明显同样涉及原始类型的封箱(boxing)。
如果你需要非 null 原始类型的数组,举个例子,优先选择 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 中,可以直接将现有的数组引用传递给 vararg 参数。 在 Kotlin,你需要使用扩展运算符:
val values = intArrayOf(1, 2, 3) printDouble(*values)
在 Java 中,数组引用按“原样”传递给函数,而不需要额外的数组分配。 但是,Kotlin 扩展运算符的编译方式有所不同,你可以在 Java 表示中看到:
[] values = []{1, 2, 3}; printDouble(Arrays.copyOf(values, values.length));
当调用该函数时,现有的数组总是被复制。其优点是这样的代码更安全:它允许函数修改数组而不影响调用者代码。 但是它分配了额外的内存。
请注意从 Kotlin 代码调用具有可变数量参数在 Java 方法具有相同的效果。
2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务