Kotlin Vocabulary | 內(nèi)聯(lián)類 inline class

  • 特定條件和情況
    這篇博客描述了一個 Kotlin 試驗(yàn)性功能,它還在調(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ò)展閱讀:

如果我們的 ID 對應(yīng)的不是 Android 資源,而是 Doggo 或 Cat 之類的域?qū)ο螅敲淳蜁茈y區(qū)分這兩個同為 Int 類型的 ID。為了實(shí)現(xiàn)類型安全,需要將 ID 包裝在一個類中,從而使狗與貓的 ID 編碼為不同的類型。這樣做的缺點(diǎn)是您要付出額外的性能成本,因?yàn)楸緛碇恍枰粋€原生類型,但是卻實(shí)例化出來了一個新的對象。

通過 Kotlin 內(nèi)聯(lián)類您可以創(chuàng)建包裝類型 (wrapper type),卻不會有額外的性能消耗。這是 Kotlin 1.3 中添加的實(shí)驗(yàn)性功能。內(nèi)聯(lián)類只能有一個屬性。在編譯時,內(nèi)聯(lián)類會在可能的地方被替換為其內(nèi)部的屬性 (取消裝箱),從而降低常規(guī)包裝類的性能成本。對于包裝對象是原生類型的情況,這尤其重要,因?yàn)榫幾g器已經(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 被裝箱了

工作原理

讓我們實(shí)現(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)建對象的新實(shí)例時,將使用公共構(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 中實(shí)例化內(nèi)聯(lián)類。

有參構(gòu)造函數(shù)是私有的,第二個構(gòu)造函數(shù)的名字中包含了一個 "-",其在 Java 中為無效字符。這意味著無法從 Java 實(shí)例化內(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 實(shí)例。

如果在可以使用原生類型的地方使用內(nèi)聯(lián)類,則 Kotlin 編譯器將知道這一點(diǎn),并會直接使用原生類型:


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

因?yàn)橹挥袑ο罂梢詾榭眨允褂帽谎b箱的實(shí)現(xiàn)。

集合


val doggos = listOf(myDoggoId)
 
// 反編譯后的 Java 代碼
doggos = CollectionsKt.listOf(DoggoId.box-impl(myDoggoId));

CollectionsKt.listOf 的方法簽名是:

fun <T> listOf(element: T): List<T>

因?yàn)榇朔椒ㄐ枰粋€對象,所以 Kotlin 編譯器將原生類型裝箱,以確保使用的是對象。

基類

fun handleId(id: Id) {}
fun myInterfaceUsage() {
    handleId(myDoggoId)
}
 
// 反編譯后的 Java 代碼
public static final void myInterfaceUsage() {
    handleId(DoggoId.box-impl(myDoggoId));
}

因?yàn)檫@里需要的參數(shù)類型是超類: Id,所以這里使用了裝箱的實(shí)現(xiàn)。

原理 —— 相等性檢查

Kotlin 編譯器會在所有可能的地方使用非裝箱類型參數(shù)。為了達(dá)到這個目的,內(nèi)聯(lián)類有三個不同的相等性檢查的方法的實(shí)現(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)。因?yàn)?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 事實(shí)上是長整型,那這里的相等性檢查就應(yīng)該是有效的。不過,因?yàn)槲覀優(yōu)榱怂鼈兊念愋桶踩褂玫氖莾?nèi)聯(lián)類,所以,接下來編譯器會首先對兩個對象進(jìn)行類型檢查,以判斷我們拿來比較的兩個對象是否為同一類型。由于它們不是同一類型,我們會看到一個編譯器報錯: "Operator == can’t be applied to long and DoggoId" (== 運(yùn)算符無法用于長整形和 DoggoId)。對編譯器來說,這種比較就好像是判斷 cat1 == doggo1 一樣,毫無疑問結(jié)果不會是 true。

doggo1.equals(1L)

這里的相等檢查可以編譯通過,因?yàn)?Kotlin 編譯器使用的 equals 方法的實(shí)現(xiàn)所需要的參數(shù)可以是一個長整形和一個 Object。但是因?yàn)檫@個方法首先會進(jìn)行類型檢查,所以相等檢查將會返回 false,因?yàn)?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ù)都是原生類型。

為了實(shí)現(xiàn)此功能,Kotlin 編譯器會改寫函數(shù)的名稱,并使用內(nèi)聯(lián)類作為函數(shù)參數(shù)。

在 Java 中使用內(nèi)聯(lián)類

我們已經(jīng)講過,不能在 Java 中實(shí)例化內(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)類實(shí)例
如果我們將內(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ù)時,就會報錯。因?yàn)榫幾g器會不知道我們到底想要調(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)驗(yàn)上來說可能會對性能產(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,則可能會遇到無法使用的情況。

最后,這仍然是一項(xiàng)實(shí)驗(yàn)性功能。它是否會發(fā)布正式版,以及正式版發(fā)布時,它的實(shí)現(xiàn)是否與現(xiàn)在相同,都還是未知數(shù)。

因此,既然您了解了內(nèi)聯(lián)類的好處和限制,就可以在是否以及何時使用它們的問題上做出明智的決定。

點(diǎn)擊這里了解更多關(guān)于用 Kotlin 進(jìn)行 Android 開發(fā)的相關(guān)資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容