Kotlin Vocabulary | 內聯類 inline class

* 特定條件和狀況 這篇博客描述了一個 Kotlin 試驗性功能,它還在調整之中。本文基於 Kotlin 1.3.50 撰寫。

類型安全幫助咱們防止出現錯誤以及避免回過頭去調試錯誤。對於 Android 資源文件,好比 String、Font 或 Animation 資源,咱們可使用 androidx.annotations,經過使用像 @StringRes、@FontRes 這樣的註解,就可讓代碼檢查工具 (如 Lint) 限制咱們只能傳遞正確類型的參數:html

fun myStringResUsage(@StringRes string: Int){ }
 
// 錯誤: 須要 String 類型的資源
myStringResUsage(1)
複製代碼

擴展閱讀:java

若是咱們的 ID 對應的不是 Android 資源,而是 Doggo 或 Cat 之類的域對象,那麼就會很難區分這兩個同爲 Int 類型的 ID。爲了實現類型安全,須要將 ID 包裝在一個類中,從而使狗與貓的 ID 編碼爲不一樣的類型。這樣作的缺點是您要付出額外的性能成本,由於原本只須要一個原生類型,可是卻實例化出來了一個新的對象。android

經過 Kotlin 內聯類您能夠建立包裝類型 (wrapper type),卻不會有額外的性能消耗。這是 Kotlin 1.3 中添加的實驗性功能。內聯類只能有一個屬性。在編譯時,內聯類會在可能的地方被替換爲其內部的屬性 (取消裝箱),從而下降常規包裝類的性能成本。對於包裝對象是原生類型的狀況,這尤爲重要,由於編譯器已經對它們進行了優化。因此將一個原始數據類型包裝在內聯類裏就意味着,在可能的狀況下,數據值會以原始數據值的形式出現。git

inline class DoggoId(val id: Long)
data class Doggo(val id: DoggoId, … )
 
// 用法
val goodDoggo = Doggo(DoggoId(doggoId), …)
fun pet(id: DoggoId) { … }
複製代碼

內聯

內聯類的惟一做用是成爲某種類型的包裝,所以 Kotlin 對其施加了許多限制:github

  • 最多一個參數 (類型不受限制)
  • 沒有 backing fields
  • 不能有 init 塊
  • 不能繼承其餘類

不過,內聯類能夠作到:數組

  • 從接口繼承
  • 具備屬性和方法
interface Id
inline class DoggoId(val id: Long) : Id {
  val stringId
  get() = id.toString()

  fun isValid()= id > 0L

}
複製代碼

⚠️ 注意: Typealias 看起來與內聯類類似,可是類型別名只是爲現有類型提供了可選名稱,而內聯類則建立了新類型。安全

聲明對象 —— 包裝仍是不包裝?

因爲內聯類相對於手動包裝類型的最大優點是對內存分配的影響,所以請務必記住,這種影響很大程度上取決於您在何處以及如何使用內聯類。通常規則是,若是將內聯類用做另外一種類型,則會對參數進行包裝 (裝箱)。bash

參數被用做其餘類型時會被裝箱。app

好比,須要在集合、數組中用到 Object 或者 Any 類型;或者須要 Object 或者 Any 做爲可空對象時。根據您比較兩個內聯類結構的方式的不一樣,會最終形成 (內聯類) 其中一個參數被裝箱,也或者全部參數都不會被裝箱。函數

val doggo1 = DoggoId(1L)
val doggo2 = DoggoId(2L)
複製代碼
  • doggo1 == doggo2 — doggo1 和 doggo2 都沒有被裝箱
  • doggo1.equals(doggo2) — doggo1 是原生類型可是 doggo2 被裝箱了

工做原理

讓咱們實現一個簡單的內聯類:

interface Id
inline class DoggoId(val id: Long) : Id
複製代碼

讓咱們逐步分析反編譯後的 Java 代碼,並分析它們對使用內聯類的影響。您能夠在下方註釋找到完整的反編譯代碼

原理 —— 構造函數

/* 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 有兩個構造函數:

  • 私有合成構造函數 DoggoId(long id)
  • 公共構造函數

建立對象的新實例時,將使用公共構造函數:

val myDoggoId = DoggoId(1L)
 
// 反編譯過的代碼
static final long myDoggoId = DoggoId.constructor-impl(1L);

複製代碼

若是嘗試使用 Java 建立 Doggo ID,則會收到一個錯誤:

DoggoId u = new DoggoId(1L);
// 錯誤: DoggoId 中的 DoggoId() 方法沒法使用 long 類型
複製代碼

您沒法在 Java 中實例化內聯類。

有參構造函數是私有的,第二個構造函數的名字中包含了一個 "-",其在 Java 中爲無效字符。這意味着沒法從 Java 實例化內聯類。

原理 —— 參數用法

/* 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);
   }
}
複製代碼

參數 id 經過兩種方式暴露給外界:

  • 經過 getId() 做爲原生類型;
  • 做爲一個對象: box_impl 方法會建立一個 DoggoId 實例。

若是在可使用原生類型的地方使用內聯類,則 Kotlin 編譯器將知道這一點,並會直接使用原生類型:

fun walkDog(doggoId: DoggoId) {}
 
// 反編譯後的 Java 代碼
public final void walkDog_Mu_n4VY(**long** doggoId) { }
複製代碼

當須要一個對象時,Kotlin 編譯器將使用原生類型的包裝版本,從而每次都建立一個新的對象。

當須要一個對象時,Kotlin 編譯器將使用原生類型的包裝版本,從而每次都建立一個新的對象,例如:

可空對象

fun pet(doggoId: DoggoId?) {}
 
// 反編譯後的 Java 代碼
public static final void pet_5ZN6hPs/* $FF was: pet-5ZN6hPs*/(@Nullable InlineDoggoId doggo) {}

複製代碼

由於只有對象能夠爲空,因此使用被裝箱的實現。

集合

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));
}
複製代碼

由於這裏須要的參數類型是超類: Id,因此這裏使用了裝箱的實現。

原理 —— 相等性檢查

Kotlin 編譯器會在全部可能的地方使用非裝箱類型參數。爲了達到這個目的,內聯類有三個不一樣的相等性檢查的方法的實現: 重寫的 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 方法會調用另外一個生成的方法: equals_impl(long, Object)。由於 equals 方法須要一個 Object 參數,因此 doggo2 的值會被裝箱,而 doggo1 將會使用原生類型:

DoggoId.equals-impl(doggo1, DoggoId.box-impl(doggo2))
複製代碼

doggo1 == doggo2

使用 == 會生成:

DoggoId.equals-impl0(doggo1, doggo2)
複製代碼

因此在使用 == 時,doggo1 和 doggo2 都會使用原生類型。

doggo1 == 1L

若是 Kotlin 能夠肯定 doggo1 事實上是長整型,那這裏的相等性檢查就應該是有效的。不過,由於咱們爲了它們的類型安全而使用的是內聯類,因此,接下來編譯器會首先對兩個對象進行類型檢查,以判斷咱們拿來比較的兩個對象是否爲同一類型。因爲它們不是同一類型,咱們會看到一個編譯器報錯: "Operator == can’t be applied to long and DoggoId" (== 運算符沒法用於長整形和 DoggoId)。對編譯器來講,這種比較就好像是判斷 cat1 == doggo1 同樣,毫無疑問結果不會是 true。

doggo1.equals(1L)
複製代碼

這裏的相等檢查能夠編譯經過,由於 Kotlin 編譯器使用的 equals 方法的實現所須要的參數能夠是一個長整形和一個 Object。可是由於這個方法首先會進行類型檢查,因此相等檢查將會返回 false,由於 Object 不是 DoggoId。

覆蓋使用原生類型和內聯類做爲參數的函數

定義一個方法時,Kotlin 編譯器容許使用原生類型和不可空內聯類做爲參數:

fun pet(doggoId: Long) {}
fun pet(doggoId: DoggoId) {}
 
// 反編譯的 Java 代碼
public static final void pet(long id) { }
public final void pet_Mu_n4VY(long doggoId) { }
複製代碼

在反編譯出的代碼中,咱們能夠看到這兩種函數,它們的參數都是原生類型。

爲了實現此功能,Kotlin 編譯器會改寫函數的名稱,並使用內聯類做爲函數參數。

在 Java 中使用內聯類

咱們已經講過,不能在 Java 中實例化內聯類。那可不可使用呢?

✅ 可以將內聯類傳遞給 Java 函數

咱們能夠將內聯類做爲參數傳遞,它們將會做爲對象被使用。咱們也能夠獲取其中包裝的屬性:

void myJavaMethod(DoggoId doggoId){
    long id = doggoId.getId();
}
複製代碼

在 Java 函數中使用內聯類實例

若是咱們將內聯類聲明爲頂層對象,就能夠在 Java 中以原生類型得到它們的引用,以下:

// Kotlin 的聲明
val doggo1 = DoggoId(1L)
 
// Java 的使用
long myDoggoId = GoodDoggosKt.getU1();

複製代碼

✅ & ❌調用參數中含有內聯類的 Kotlin 函數

若是咱們有一個 Java 函數,它接收一個內聯類對象做爲參數。函數中調用一個一樣接收內聯類做爲參數的 Kotlin 函數。這種狀況下,咱們會看到一個編譯器報錯:

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)
}
複製代碼

若是在一個類中,咱們分別覆蓋了使用內聯類做爲參數和使用底層類型做爲參數的兩個函數,當咱們從 Java 中調用這些函數時,就會報錯。由於編譯器會不知道咱們到底想要調用哪一個函數:

fun pet(doggoId: Long) {}

fun pet(doggoId: DoggoId) {}

// Java
TestInlineKt.pet(1L);

Error: Ambiguous method call. Both pet(long) and pet(long) match
複製代碼

內聯類: 使用仍是不使用,這是一個問題

類型安全能夠幫助咱們寫出更健壯的代碼,可是經驗上來講可能會對性能產生不利的影響。內聯類提供了一個一箭雙鵰的解決方案 —— 沒有額外消耗的類型安全。因此咱們就應該老是使用它們嗎?

內聯類帶來了一系列的限制,使得您建立的對象只能作一件事: 成爲包裝器。這意味着將來,不熟悉這段代碼的開發者,也無法像在數據類中那樣,能夠給構造函數添加參數,從而致使類的複雜度被錯誤地增長。

在性能方面,咱們已經看到 Kotlin 編譯器會盡其所能使用底層類型,但在許多狀況下仍然會建立新對象。

在 Java 中使用內聯類時仍然有諸多限制,若是您尚未徹底遷移到 Kotlin,則可能會遇到沒法使用的狀況。

最後,這仍然是一項實驗性功能。它是否會發布正式版,以及正式版發佈時,它的實現是否與如今相同,都仍是未知數。

所以,既然您瞭解了內聯類的好處和限制,就能夠在是否以及什麼時候使用它們的問題上作出明智的決定。

點擊這裏瞭解更多關於用 Kotlin 進行 Android 開發的相關資料

相關文章
相關標籤/搜索