- 特定條件和情況
這篇博客描述了一個 Kotlin 試驗性功能奈搜,它還在調(diào)整之中庇茫。本文基于 Kotlin 1.3.50 撰寫。
類型安全幫助我們防止出現(xiàn)錯誤以及避免回過頭去調(diào)試錯誤。對于 Android 資源文件鸟赫,比如 String淤毛、Font 或 Animation 資源秋度,我們可以使用 androidx.annotations,通過使用像 @StringRes钱床、@FontRes 這樣的注解荚斯,就可以讓代碼檢查工具 (如 Lint) 限制我們只能傳遞正確類型的參數(shù):
fun myStringResUsage(@StringRes string: Int){ }
// 錯誤: 需要 String 類型的資源
myStringResUsage(1)
擴(kuò)展閱讀:
- 利用注釋改進(jìn)代碼檢查
https://developer.android.google.cn/studio/write/annotations
如果我們的 ID 對應(yīng)的不是 Android 資源,而是 Doggo 或 Cat 之類的域?qū)ο蟛榕疲敲淳蜁茈y區(qū)分這兩個同為 Int 類型的 ID事期。為了實現(xiàn)類型安全,需要將 ID 包裝在一個類中纸颜,從而使狗與貓的 ID 編碼為不同的類型兽泣。這樣做的缺點是您要付出額外的性能成本,因為本來只需要一個原生類型胁孙,但是卻實例化出來了一個新的對象唠倦。
通過 Kotlin 內(nèi)聯(lián)類您可以創(chuàng)建包裝類型 (wrapper type),卻不會有額外的性能消耗涮较。這是 Kotlin 1.3 中添加的實驗性功能稠鼻。內(nèi)聯(lián)類只能有一個屬性。在編譯時狂票,內(nèi)聯(lián)類會在可能的地方被替換為其內(nèi)部的屬性 (取消裝箱)候齿,從而降低常規(guī)包裝類的性能成本。對于包裝對象是原生類型的情況,這尤其重要慌盯,因為編譯器已經(jīng)對它們進(jìn)行了優(yōu)化周霉。所以將一個原始數(shù)據(jù)類型包裝在內(nèi)聯(lián)類里就意味著,在可能的情況下亚皂,數(shù)據(jù)值會以原始數(shù)據(jù)值的形式出現(xiàn)俱箱。
inline class DoggoId(val id: Long)
data class Doggo(val id: DoggoId, … )
// 用法
val goodDoggo = Doggo(DoggoId(doggoId), …)
fun pet(id: DoggoId) { … }
內(nèi)聯(lián)
內(nèi)聯(lián)類的唯一作用是成為某種類型的包裝,因此 Kotlin 對其施加了許多限制:
- 最多一個參數(shù) (類型不受限制)
- 沒有 backing fields
- 不能有 init 塊
- 不能繼承其他類
不過灭必,內(nèi)聯(lián)類可以做到:
- 從接口繼承
- 具有屬性和方法
interface Id
inline class DoggoId(val id: Long) : Id {
val stringId
get() = id.toString()
fun isValid()= id > 0L
}
?? 注意: Typealias 看起來與內(nèi)聯(lián)類相似匠楚,但是類型別名只是為現(xiàn)有類型提供了可選名稱,而內(nèi)聯(lián)類則創(chuàng)建了新類型厂财。
聲明對象 —— 包裝還是不包裝芋簿?
由于內(nèi)聯(lián)類相對于手動包裝類型的最大優(yōu)勢是對內(nèi)存分配的影響,因此請務(wù)必記住璃饱,這種影響很大程度上取決于您在何處以及如何使用內(nèi)聯(lián)類贮喧。一般規(guī)則是腾啥,如果將內(nèi)聯(lián)類用作另一種類型竣稽,則會對參數(shù)進(jìn)行包裝 (裝箱)试吁。
參數(shù)被用作其他類型時會被裝箱。
比如谒撼,需要在集合食寡、數(shù)組中用到 Object 或者 Any 類型;或者需要 Object 或者 Any 作為可空對象時廓潜。根據(jù)您比較兩個內(nèi)聯(lián)類結(jié)構(gòu)的方式的不同抵皱,會最終造成 (內(nèi)聯(lián)類) 其中一個參數(shù)被裝箱,也或者所有參數(shù)都不會被裝箱辩蛋。
val doggo1 = DoggoId(1L)
val doggo2 = DoggoId(2L)
- doggo1 == doggo2 — doggo1 和 doggo2 都沒有被裝箱
- doggo1.equals(doggo2) — doggo1 是原生類型但是 doggo2 被裝箱了
工作原理
讓我們實現(xiàn)一個簡單的內(nèi)聯(lián)類:
interface Id
inline class DoggoId(val id: Long) : Id
讓我們逐步分析反編譯后的 Java 代碼呻畸,并分析它們對使用內(nèi)聯(lián)類的影響。您可以在下方注釋找到完整的反編譯代碼悼院。
原理 —— 構(gòu)造函數(shù)
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
// $FF: synthetic method
private DoggoId(long id) {
this.id = id;
}
public static long constructor_impl/* $FF was: constructor-impl*/(long id) {
return id;
}
}
DoggoId 有兩個構(gòu)造函數(shù):
- 私有合成構(gòu)造函數(shù) DoggoId(long id)
- 公共構(gòu)造函數(shù)
創(chuàng)建對象的新實例時伤为,將使用公共構(gòu)造函數(shù):
val myDoggoId = DoggoId(1L)
// 反編譯過的代碼
static final long myDoggoId = DoggoId.constructor-impl(1L);
如果嘗試使用 Java 創(chuàng)建 Doggo ID,則會收到一個錯誤:
DoggoId u = new DoggoId(1L);
// 錯誤: DoggoId 中的 DoggoId() 方法無法使用 long 類型
您無法在 Java 中實例化內(nèi)聯(lián)類据途。
有參構(gòu)造函數(shù)是私有的绞愚,第二個構(gòu)造函數(shù)的名字中包含了一個 "-",其在 Java 中為無效字符颖医。這意味著無法從 Java 實例化內(nèi)聯(lián)類位衩。
原理 —— 參數(shù)用法
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
private final long id;
public final long getId() {
return this.id;
}
// $FF: synthetic method
@NotNull
public static final DoggoId box_impl/* $FF was: box-impl*/(long v) {
return new DoggoId(v);
}
}
參數(shù) id 通過兩種方式暴露給外界:
- 通過 getId() 作為原生類型;
- 作為一個對象: box_impl 方法會創(chuàng)建一個 DoggoId 實例便脊。
如果在可以使用原生類型的地方使用內(nèi)聯(lián)類蚂四,則 Kotlin 編譯器將知道這一點光戈,并會直接使用原生類型:
fun walkDog(doggoId: DoggoId) {}
// 反編譯后的 Java 代碼
public final void walkDog_Mu_n4VY(**long** doggoId) { }
當(dāng)需要一個對象時哪痰,Kotlin 編譯器將使用原生類型的包裝版本遂赠,從而每次都創(chuàng)建一個新的對象。
當(dāng)需要一個對象時晌杰,Kotlin 編譯器將使用原生類型的包裝版本跷睦,從而每次都創(chuàng)建一個新的對象,例如:
可空對象
fun pet(doggoId: DoggoId?) {}
// 反編譯后的 Java 代碼
public static final void pet_5ZN6hPs/* $FF was: pet-5ZN6hPs*/(@Nullable I
因為只有對象可以為空肋演,所以使用被裝箱的實現(xiàn)抑诸。
集合
val doggos = listOf(myDoggoId)
// 反編譯后的 Java 代碼
doggos = CollectionsKt.listOf(DoggoId.box-impl(myDoggoId));
CollectionsKt.listOf 的方法簽名是:
fun <T> listOf(element: T): List<T>
因為此方法需要一個對象,所以 Kotlin 編譯器將原生類型裝箱爹殊,以確保使用的是對象蜕乡。
基類
fun handleId(id: Id) {}
fun myInterfaceUsage() {
handleId(myDoggoId)
}
// 反編譯后的 Java 代碼
public static final void myInterfaceUsage() {
handleId(DoggoId.box-impl(myDoggoId));
}
因為這里需要的參數(shù)類型是超類: Id,所以這里使用了裝箱的實現(xiàn)梗夸。
原理 —— 相等性檢查
Kotlin 編譯器會在所有可能的地方使用非裝箱類型參數(shù)层玲。為了達(dá)到這個目的,內(nèi)聯(lián)類有三個不同的相等性檢查的方法的實現(xiàn): 重寫的 equals 方法和兩個自動生成的方法:
/* Copyright 2019 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
public static boolean equals_impl/* $FF was: equals-impl*/(long var0, @Nullable Object var2) {
if (var2 instanceof DoggoId) {
long var3 = ((DoggoId)var2).unbox-impl();
if (var0 == var3) {
return true;
}
}
return false;
}
public static final boolean equals_impl0/* $FF was: equals-impl0*/(long p1, long p2) {
return p1 == p2;
}
public boolean equals(Object var1) {
return equals-impl(this.id, var1);
}
}
doggo1.equals(doggo2)
這種情況下反症,equals 方法會調(diào)用另一個生成的方法: equals_impl(long, Object)辛块。因為 equals 方法需要一個 Object 參數(shù),所以 doggo2 的值會被裝箱铅碍,而 doggo1 將會使用原生類型:
DoggoId.equals-impl(doggo1, DoggoId.box-impl(doggo2))
doggo1 == doggo2
使用 == 會生成:
DoggoId.equals-impl0(doggo1, doggo2)
所以在使用 == 時润绵,doggo1 和 doggo2 都會使用原生類型。
doggo1 == 1L
如果 Kotlin 可以確定 doggo1 事實上是長整型胞谈,那這里的相等性檢查就應(yīng)該是有效的尘盼。不過,因為我們?yōu)榱怂鼈兊念愋桶踩褂玫氖莾?nèi)聯(lián)類烦绳,所以悔叽,接下來編譯器會首先對兩個對象進(jìn)行類型檢查,以判斷我們拿來比較的兩個對象是否為同一類型爵嗅。由于它們不是同一類型娇澎,我們會看到一個編譯器報錯: "Operator == can’t be applied to long and DoggoId" (== 運算符無法用于長整形和 DoggoId)。對編譯器來說睹晒,這種比較就好像是判斷 cat1 == doggo1 一樣趟庄,毫無疑問結(jié)果不會是 true。
doggo1.equals(1L)
這里的相等檢查可以編譯通過伪很,因為 Kotlin 編譯器使用的 equals 方法的實現(xiàn)所需要的參數(shù)可以是一個長整形和一個 Object戚啥。但是因為這個方法首先會進(jìn)行類型檢查,所以相等檢查將會返回 false锉试,因為 Object 不是 DoggoId猫十。
覆蓋使用原生類型和內(nèi)聯(lián)類作為參數(shù)的函數(shù)
定義一個方法時,Kotlin 編譯器允許使用原生類型和不可空內(nèi)聯(lián)類作為參數(shù):
fun pet(doggoId: Long) {}
fun pet(doggoId: DoggoId) {}
// 反編譯的 Java 代碼
public static final void pet(long id) { }
public final void pet_Mu_n4VY(long doggoId) { }
在反編譯出的代碼中,我們可以看到這兩種函數(shù)拖云,它們的參數(shù)都是原生類型贷笛。
為了實現(xiàn)此功能,Kotlin 編譯器會改寫函數(shù)的名稱宙项,并使用內(nèi)聯(lián)類作為函數(shù)參數(shù)乏苦。
在 Java 中使用內(nèi)聯(lián)類
我們已經(jīng)講過,不能在 Java 中實例化內(nèi)聯(lián)類尤筐。那可不可以使用呢汇荐?
? 可以將內(nèi)聯(lián)類傳遞給 Java 函數(shù)
我們可以將內(nèi)聯(lián)類作為參數(shù)傳遞,它們將會作為對象被使用盆繁。我們也可以獲取其中包裝的屬性:
void myJavaMethod(DoggoId doggoId){
long id = doggoId.getId();
}
? 在 Java 函數(shù)中使用內(nèi)聯(lián)類實例
如果我們將內(nèi)聯(lián)類聲明為頂層對象掀淘,就可以在 Java 中以原生類型獲得它們的引用,如下:
// Kotlin 的聲明
val doggo1 = DoggoId(1L)
// Java 的使用
long myDoggoId = GoodDoggosKt.getU1();
? & ?調(diào)用參數(shù)中含有內(nèi)聯(lián)類的 Kotlin 函數(shù)
如果我們有一個 Java 函數(shù)油昂,它接收一個內(nèi)聯(lián)類對象作為參數(shù)革娄。函數(shù)中調(diào)用一個同樣接收內(nèi)聯(lián)類作為參數(shù)的 Kotlin 函數(shù)。這種情況下秕狰,我們會看到一個編譯器報錯:
fun pet(doggoId: DoggoId) {}
// Java
void petInJava(doggoId: DoggoId){
pet(doggoId)
// 編譯器報錯: pet(long) cannot be applied to pet(DoggoId) (pet(長整形) 不能用于 pet(DoggoId))
}
對于 Java 來說稠腊,DoggoId 是一個新類型,但是編譯器生成的 pet(long) 和 pet(DoggoId) 并不存在鸣哀。
但是架忌,我們還是可以傳遞底層類型:
fun pet(doggoId: DoggoId) {}
// Java
void petInJava(doggoId: DoggoId){
pet(doggoId.getId)
}
如果在一個類中,我們分別覆蓋了使用內(nèi)聯(lián)類作為參數(shù)和使用底層類型作為參數(shù)的兩個函數(shù)我衬,當(dāng)我們從 Java 中調(diào)用這些函數(shù)時叹放,就會報錯。因為編譯器會不知道我們到底想要調(diào)用哪個函數(shù):
fun pet(doggoId: Long) {}
fun pet(doggoId: DoggoId) {}
// Java
TestInlineKt.pet(1L);
Error: Ambiguous method call. Both pet(long) and pet(long) matc
內(nèi)聯(lián)類: 使用還是不使用挠羔,這是一個問題
類型安全可以幫助我們寫出更健壯的代碼井仰,但是經(jīng)驗上來說可能會對性能產(chǎn)生不利的影響。內(nèi)聯(lián)類提供了一個兩全其美的解決方案 —— 沒有額外消耗的類型安全破加。所以我們就應(yīng)該總是使用它們嗎俱恶?
內(nèi)聯(lián)類帶來了一系列的限制,使得您創(chuàng)建的對象只能做一件事: 成為包裝器范舀。這意味著未來合是,不熟悉這段代碼的開發(fā)者,也沒法像在數(shù)據(jù)類中那樣锭环,可以給構(gòu)造函數(shù)添加參數(shù)聪全,從而導(dǎo)致類的復(fù)雜度被錯誤地增加。
在性能方面辅辩,我們已經(jīng)看到 Kotlin 編譯器會盡其所能使用底層類型难礼,但在許多情況下仍然會創(chuàng)建新對象娃圆。
在 Java 中使用內(nèi)聯(lián)類時仍然有諸多限制,如果您還沒有完全遷移到 Kotlin蛾茉,則可能會遇到無法使用的情況讼呢。
最后,這仍然是一項實驗性功能臀稚。它是否會發(fā)布正式版吝岭,以及正式版發(fā)布時三痰,它的實現(xiàn)是否與現(xiàn)在相同吧寺,都還是未知數(shù)。
因此散劫,既然您了解了內(nèi)聯(lián)類的好處和限制稚机,就可以在是否以及何時使用它們的問題上做出明智的決定。
點擊這里了解更多關(guān)于用 Kotlin 進(jìn)行 Android 開發(fā)的相關(guān)資料