博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【译】探索 Kotlin 中的隐性成本(第二部分)
阅读量:6192 次
发布时间:2019-06-21

本文共 4626 字,大约阅读时间需要 15 分钟。

本文讲的是【译】探索 Kotlin 中的隐性成本(第二部分),
  • 原文地址:
  • 原文作者:
  • 译文出自:
  • 本文永久链接:
  • 译者:
  • 校对者: 、

局部函数,空值安全和可变参数

本文是正在进行中的 Kotlin 编程语言系列的第二部分。如果你还未读过的话,别忘了去看一下。

让我们重新看一下 Kotlin 的本质,去发现更多 Kotlin 特性的实现细节。

局部函数

有一种函数我们在第一篇文章没有讲到:使用常规语法在其他函数内部声明的函数。这是,它们可以访问外部函数的作用域。

fun someMath(a: Int): Int {    fun sumSquare(b: Int) = (a + b) * (a + b)    return sumSquare(1) + sumSquare(2)}

让我们先来谈谈他们最大的局限性:局部函数不能被声明为内联(还不能?)并且一个包含局部函数的函数也不能被声明为内联。还没有一个神奇的方法可以避免在这种情况下函数调用的成本。

局部函数在编译后被转换为 Function 对象,就像 lambdas 那样,并且有着和上篇文章中描述的关于非内联函数的大多数相同的限制。编译之后的 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);}

但是与 lambdas 相比有一个小的性能损失:由于调用者是知道这个函数的真正实例的,它的特定方法将被直接调用,而不是调用来自 Function 接口的通用合成方法。这意味着当从外部函数调用局部函数的时候不会有强制类型转换或者基础类型装箱现象发生。我们可以通过查看字节码来验证这一点:

ALOAD 1ICONST_1INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)IALOAD 1ICONST_2INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)IIADDIRETURN

我们可以看到那个被调用了两次的方法就是那个接收一个 int 参数并且返回一个 int 的方法,那个加法被立即执行并且没有任何中间的拆箱操作。

当然,在每次方法调用的过程中仍然有着创建一个新 Function 对象的成本。这个成本可以通过将局部函数重写为非捕获性的来避免:

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 中有三种数组类型:

  • IntArrayFloatArray 还有其他的:基础类型数组。编译为 int[]float[] 和其他的类型。
  • Array<T>:不可空对象引用类型化数组,这涉及到对基础类型的装箱。
  • Array<T?>:可空对象引用类型化数组。很明显,这也涉及到基础类型的装箱。

如果你需要一个不可空的基础类型数组,最好用 IntArray 而不是 Array<Int> 来避免装箱(操作)。


可变参数

Kotlin 允许声明具有的函数,就像 Java 那样。声明语法有点不一样:

fun printDouble(vararg values: Int) {    values.forEach { println(it * 2) }}

就像 Java 中那样,vararg 参数实际上被编译为一个给定类型的 数组 参数。你可以用三种不同的方式来调用这些函数:

1. 传入多个参数

printDouble(1, 2, 3)

Kotlin 编译器会将这行代码转化为创建并初始化一个新的数组,和 Java 编译器做的完全一样:

printDouble(new int[]{1, 2, 3});

因此有创建一个新数组的开销,但与 Java 相比这并不是什么新鲜事。

2. 传入一个单独的数组

这就是不同之处。在 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 方法会产生相同的效果。

3. 传入混合的数组和参数

分布操作符主要的好处是,它还允许在同一个调用中数组参数和其他参数混合在一起进行传递。

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 中调用一个具有可变参数的函数时会增加创建一个新临时数组的成本,即使是使用已有数组的值。对方法被反复调用的性能关键性的代码来说,考虑添加一个以真正的数组而不是 可变数组 为参数的方法。

原文发布时间为:2017年7月20日
本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。

转载地址:http://kigda.baihongyu.com/

你可能感兴趣的文章
Apache设置禁止访问网站目录(目录列表显示文件)
查看>>
寻找精力与效率的平衡点
查看>>
1048. 数字加密(20)
查看>>
指针形式访问字符串
查看>>
17 常用模块 tiime os sys 递归 序列化
查看>>
BZOJ1775[USACO 2009 Dec Gold 3.Video Game Troubles]——DP
查看>>
轻量级富文本编辑器wangEditor
查看>>
Treasure Hunt IV 找规律题
查看>>
深入理解Javascript中this, prototype, constructor
查看>>
SpringMvc (注解)中的上传文件
查看>>
putty xming gnome-session
查看>>
C# - 操作符
查看>>
返回一个整数数组中最大子数组的和
查看>>
一道面试题细说C++类型转换
查看>>
让python输出不自行换行的方法
查看>>
Django学了2个月了,没有什么进展,但是不放弃!
查看>>
sql if
查看>>
IP地址转换成Long型数字
查看>>
转载的一篇嵌入式大佬经验博文
查看>>
Solaris进程管理
查看>>