- 原文地址: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、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。