【譯】探索 Kotlin 中的隱性成本(第二部分)

局部函數,空值安全和可變參數

本文是正在進行中的 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 參數實際上被編譯爲一個給定類型的 數組 參數。你能夠用三種不一樣的方式來調用這些函數:

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 中調用一個具備可變參數的函數時會增長建立一個新臨時數組的成本,即便是使用已有數組的值。對方法被反覆調用的性能關鍵性的代碼來講,考慮添加一個以真正的數組而不是 可變數組 爲參數的方法。


感謝閱讀,若是你喜歡的話請分享本文。

繼續閱讀第三部分委派屬性範圍


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索