- 原文地址:Exploring Kotlin’s hidden costs — Part 3
- 原文做者:Christophe B.
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:PhxNirvana
- 校對者:Zhiw、Feximin
本系列關於 Kotlin 的前兩篇文章發表以後,讀者們紛至沓來的讚譽讓我受寵若驚,其中還包括 Jake Wharton 的留言。很樂意和你們再次開始探索之旅。不要錯過 第一部分 和 第二部分.html
本文咱們將探索更多關於 Kotlin 編譯器的祕密,並提供一些可使代碼更高效的建議。前端
委託屬性 是一種經過委託實現擁有 getter 和可選 setter 的 屬性,並容許實現可複用的自定義屬性。java
class Example {
var p: String by Delegate()
}複製代碼
委託對象必須實現一個擁有 getValue()
方法的操做符,以及 setValue()
方法來實現讀/寫屬性。些方法將會接受包含對象實例以及屬性元數據做爲額外參數。react
當一個類聲明委託屬性時,編譯器生成的代碼會和以下 Java 代碼類似。android
public final class Example {
@NotNull
private final Delegate p$delegate = new Delegate();
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Example.class), "p", "getP()Ljava/lang/String;"))};
@NotNull
public final String getP() {
return this.p$delegate.getValue(this, $$delegatedProperties[0]);
}
public final void setP(@NotNull String var1) {
Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
this.p$delegate.setValue(this, $$delegatedProperties[0], var1);
}
}複製代碼
一些靜態屬性元數據被加入到類中,委託在類的構造函數中初始化,並在每次讀寫屬性時調用。ios
在上面的例子中,建立了一個新的委託實例來實現屬性。這就要求委託的實現是有狀態的,例如當其內部緩存計算結果時:git
class StringDelegate {
private var cache: String? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
var result = cache
if (result == null) {
result = someOperation()
cache = result
}
return result
}
}複製代碼
與此同時,當須要額外的參數時,須要創建新的委託實例,並將其傳遞到構造器中:github
class Example {
private val nameView by BindViewDelegate<TextView>(R.id.name)
}複製代碼
但也有一些狀況是只須要一個委託實例來實現任何屬性的:當委託是無狀態,而且它所須要的惟一變量就是已經提供好的包含對象實例和委託名稱時,能夠經過將其聲明爲 object
來替代 class
實現一個單例委託。後端
舉個例子,下面的單例委託從 Android Activity
中取回與給定 tag 相匹配的 Fragment
:api
object FragmentDelegate {
operator fun getValue(thisRef: Activity, property: KProperty<*>): Fragment? {
return thisRef.fragmentManager.findFragmentByTag(property.name)
}
}複製代碼
相似地,任何已有類均可以經過擴展變成委託。getValue()
和 setValue()
也能夠被聲明成 擴展方法 來實現。Kotlin 已經提供了內置的擴展方法來容許將 Map
and MutableMap
實例用做委託,屬性名做爲其中的鍵。
若是你選擇複用相同的局部委託實例來在一個類中實現多屬性,你須要在構造函數中初始化實例。
注意:從 Kotlin 1.1 開始,也能夠聲明 方法局部變量聲明爲委託屬性。在這種狀況下,委託能夠直到該變量在方法內部聲明的時候纔去初始化,而沒必要在構造函數中就執行初始化。
類中聲明的每個委託屬性都會涉及到與之關聯委託對象的開銷,並會在類中增長一些元數據。
若是可能的話,儘可能在不一樣的屬性間複用委託。
同時也要考慮一下若是須要聲明大量委託時,委託屬性是否是一個好的選擇。
委託方法也能夠被聲明成泛型的,這樣一來不一樣類型的屬性就能夠複用同一個委託類了。
private var maxDelay: Long by SharedPreferencesDelegate<Long>()複製代碼
然而,若是像上例那樣對基本類型使用泛型委託的話,即使聲明的基本類型非空,也會在每次讀寫屬性的時候觸發裝箱和拆箱的操做。
對於非空基本類型的委託屬性來講,最好使用給定類型的特定委託類而不是泛型委託來避免每次訪問屬性時增長裝箱的額外開銷。
針對常見情形,Kotlin 提供了一些標準委託,如 Delegates.notNull()
、 Delegates.observable()
和 lazy()
。
lazy()
是一個在第一次讀取時經過給定的 lambda 值來計算屬性的初值,並返回只讀屬性的委託。
private val dateFormat: DateFormat by lazy {
SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}複製代碼
這是一種簡潔的延遲高消耗的初始化至其真正須要時的方式,在保留代碼可讀性的同時提高了性能。
須要注意的是,lazy()
並非內聯函數,傳入的 lambda 參數也會被編譯成一個額外的 Function
類,而且不會被內聯到返回的委託對象中。
常常被忽略的一點是 lazy()
有可選的 mode
參數 來決定應該返回 3 種委託的哪種:
public fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
public fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}複製代碼
默認模式 LazyThreadSafetyMode.SYNCHRONIZED
將提供相對耗費昂貴的 雙重檢查鎖 來保證一旦屬性能夠從多線程讀取時初始化塊能夠安全地執行。
若是你確信屬性只會在單線程(如主線程)被訪問,那麼能夠選擇 LazyThreadSafetyMode.NONE
來代替,從而避免使用鎖的額外開銷。
val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) {
SimpleDateFormat("dd-MM-yyyy", Locale.getDefault())
}複製代碼
使用
lazy()
委託來延遲初始化時的大量開銷以及指定模式來避免沒必要要的鎖。
區間 是 Kotlin 中用來表明一個有限的值集合的特殊表達式。值能夠是任何 Comparable
類型。 這些表達式的形式都是建立聲明瞭 ClosedRange
接口的方法。建立區間的主要方法是 ..
操做符方法。
區間表達式的主要做用是使用 in
和 !in
操做符實現包含和不包含。
if (i in 1..10) {
println(i)
}複製代碼
該實現針對非空基本類型的區間(包括 Int
、Long
、Byte
、Short
、Float
、Double
以及 Char
的值)實現了優化,因此上面的代碼能夠被優化成這樣:
if(1 <= i && i <= 10) {
System.out.println(i);
}複製代碼
零額外支出而且沒有額外對象開銷。區間也能夠被包含在 when
表達式中:
val message = when (statusCode) {
in 200..299 -> "OK"
in 300..399 -> "Find it somewhere else"
else -> "Oops"
}複製代碼
相比一系列的 if{...} else if{...}
代碼塊,這段代碼在不下降效率的同時提升了代碼的可讀性。
然而,若是在聲明和使用之間有至少一次間接調用的話,range 會有一些微小的額外開銷。好比下面的代碼:
private val myRange get() = 1..10
fun rangeTest(i: Int) {
if (i in myRange) {
println(i)
}
}複製代碼
在編譯後會建立一個額外的 IntRange
對象:
private final IntRange getMyRange() {
return new IntRange(1, 10);
}
public final void rangeTest(int i) {
if(this.getMyRange().contains(i)) {
System.out.println(i);
}
}複製代碼
將屬性的 getter 聲明爲 inline
的方法也沒法避免這個對象的建立。這是 Kotlin 1.1 編譯器能夠優化的一個點。至少經過這些特定的區間類避免了裝箱操做。
儘可能在使用時直接聲明非空基本類型的區間,不要間接調用,來避免額外區間類的建立。
或者直接聲明爲常量來複用。
區間也能夠用於其餘實現了 Comparable
的非基本類型。
if (name in "Alfred".."Alicia") {
println(name)
}複製代碼
在這種狀況下,最終實現並不會優化,並且老是會建立一個 ClosedRange
對象,以下面編譯後的代碼所示:
if(RangesKt.rangeTo((Comparable)"Alfred", (Comparable)"Alicia")
.contains((Comparable)name)) {
System.out.println(name);
}複製代碼
若是你須要對一個實現了
Comparable
的非基本類型的區間進行頻繁的包含的話,考慮將這個區間聲明爲常量來避免重複建立區間類吧。
整型區間 (除了 Float
和 Double
以外其餘的基本類型)也是 級數:它們能夠被迭代。這就能夠將經典 Java 的 for
循環用一個更短的表達式替代。
for (i in 1..10) {
println(i)
}複製代碼
通過編譯器優化後的代碼實現了零額外開銷:
int i = 1;
byte var3 = 10;
if(i <= var3) {
while(true) {
System.out.println(i);
if(i == var3) {
break;
}
++i;
}
}複製代碼
若是要反向迭代,可使用 downTo()
中綴方法來代替 ..
:
for (i in 10 downTo 1) {
println(i)
}複製代碼
編譯以後,這也實現了零額外開銷:
int i = 10;
byte var3 = 1;
if(i >= var3) {
while(true) {
System.out.println(i);
if(i == var3) {
break;
}
--i;
}
}複製代碼
然而,其餘迭代器參數並無如此好的優化。
反向迭代還有一種結果相同的方式,使用 reversed()
方法結合區間:
for (i in (1..10).reversed()) {
println(i)
}複製代碼
編譯後的代碼並無看起來那麼少:
IntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1, 10)));
int i = var10000.getFirst();
int var3 = var10000.getLast();
int var4 = var10000.getStep();
if(var4 > 0) {
if(i > var3) {
return;
}
} else if(i < var3) {
return;
}
while(true) {
System.out.println(i);
if(i == var3) {
return;
}
i += var4;
}複製代碼
會建立一個臨時的 IntRange
對象來表明區間,而後建立另外一個 IntProgression
對象來反轉前者的值。
事實上,任何結合不止一個方法來建立遞進都會生成相似的至少建立兩個微小遞進對象的代碼。
這個規則也適用於使用 step()
中綴方法來操做遞進的步驟,即便只有一步:
for (i in 1..10 step 2) {
println(i)
}複製代碼
一個次要提示,當生成的代碼讀取 IntProgression
的 last
屬性時會經過對邊界和步長的小小計算來決定準確的最後值。在上面的代碼中,最終值是 9。
最後,until()
中綴函數對於迭代也頗有用,該函數(執行結果)不包含最大值。
for (i in 0 until size) {
println(i)
}複製代碼
遺憾的是,編譯器並無針對這個經典的包含區間圍優化,迭代器依然會建立區間對象:
IntRange var10000 = RangesKt.until(0, size);
int i = var10000.getFirst();
int var1 = var10000.getLast();
if(i <= var1) {
while(true) {
System.out.println(i);
if(i == var1) {
break;
}
++i;
}
}複製代碼
這是 Kotlin 1.1 能夠提高的另外一個點
與此同時,能夠經過這樣寫來優化代碼:
for (i in 0..size - 1) {
println(i)
}複製代碼
for
循環內部的迭代,最好只用區間表達式的一個單獨方法來調用..
或downTo()
來避免額外臨時遞進對象的建立。
做爲 for
循環的替代,使用區間內聯的擴展方法 forEach()
來實現類似的效果可能更吸引人。
(1..10).forEach {
println(it)
}複製代碼
但若是仔細觀察這裏使用的 forEach()
方法簽名的話,你就會注意到並無優化區間,而只是優化了 Iterable
,因此須要建立一個 iterator。下面是編譯後代碼的 Java 形式:
Iterable $receiver$iv = (Iterable)(new IntRange(1, 10));
Iterator var1 = $receiver$iv.iterator();
while(var1.hasNext()) {
int element$iv = ((IntIterator)var1).nextInt();
System.out.println(element$iv);
}複製代碼
這段代碼相比前者更爲低效,緣由是爲了建立一個 IntRange
對象,還須要額外建立 IntIterator
。但至少它仍是生成了基本類型的值。
迭代區間時,最好只使用
for
循環而不是區間上的forEach()
方法來避免額外建立一個迭代器。
Kotlin 標準庫提供了內置的 indices
擴展屬性來生成數組和 Collection
的區間。
val list = listOf("A", "B", "C")
for (i in list.indices) {
println(list[i])
}複製代碼
使人驚訝的是,對這個 indices
的迭代獲得了編譯器的優化:
List list = CollectionsKt.listOf(new String[]{"A", "B", "C"});
int i = 0;
int var2 = ((Collection)list).size() - 1;
if(i <= var2) {
while(true) {
Object var3 = list.get(i);
System.out.println(var3);
if(i == var2) {
break;
}
++i;
}
}複製代碼
從上面的代碼中咱們能夠看到沒有建立 IntRange
對象,列表的迭代是以最高效率的方式運行的。
這適用於數組和實現了 Collection
的類,因此你若是指望相同的迭代器性能的話,能夠嘗試在特定的類上使用本身的 indices
擴展屬性。
inline val SparseArray<*>.indices: IntRange
get() = 0..size() - 1
fun printValues(map: SparseArray<String>) {
for (i in map.indices) {
println(map.valueAt(i))
}
}複製代碼
但編譯以後,咱們能夠發現這並無那麼高效率,由於編譯器沒法足夠智能地避免區間對象的產生:
public static final void printValues(@NotNull SparseArray map) {
Intrinsics.checkParameterIsNotNull(map, "map");
IntRange var10002 = new IntRange(0, map.size() - 1);
int i = var10002.getFirst();
int var2 = var10002.getLast();
if(i <= var2) {
while(true) {
Object $receiver$iv = map.valueAt(i);
System.out.println($receiver$iv);
if(i == var2) {
break;
}
++i;
}
}
}複製代碼
因此,我會建議你避免聲明自定義的 lastIndex
擴展屬性:
inline val SparseArray<*>.lastIndex: Int
get() = size() - 1
fun printValues(map: SparseArray<String>) {
for (i in 0..map.lastIndex) {
println(map.valueAt(i))
}
}複製代碼
當迭代沒有聲明
Collection
的自定義集合 時,直接在for
循環中寫本身的序列區間而不是依賴方法或屬性來生成區間,從而避免區間對象的建立。
我在寫本文時興趣盎然,但願你讀起來也同樣。可能你還期待之後有更多的文章,但這三篇已經涵蓋了我目前想要寫的全部內容了。若是喜歡的話請分享。謝謝!
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。