【再學Kotlin】換個名字可還行?typealias 和 inline class 的使用及背後實現

前言

Kotlin 的語法糖用起來很爽,但咱們不該只知足於會用的狀態。本系列文章介紹 Kotlin 關鍵字的使用以及其背後的實現html

本文摘自 Kotlin Vocabulary 系列文章,原文請移步 Alter type with typealiasZero-cost* abstractions in Kotlinjava

typealias 的使用

使用 Java 開發一段時間可能以爲 Java 中的變量名太長了!雖然優秀的命名最好是望文知義,但一堆很長的變量很影響可讀性。android

C 和 C++ 中提供了 typedef 關鍵字來定義別名,而 Kotlin 中也有相似的存在git

typealias 容許在不引入新類型的狀況下爲類或函數類型提供別名github

可使用 typealias 命名函數類型安全

typealias TeardownLogic = () -> Unit
fun onCancel(teardown : TeardownLogic){ }

private typealias OnDoggoClick = (dog: Pet.GoodDoggo) -> Unit
val onClick: OnDoggoClick
複製代碼

這樣作的缺點是名稱隱藏了傳遞給函數的參數,從而下降了可讀性app

typealias TeardownLogic = () -> Unit 
typealias TeardownLogic = (exception: Exception) -> Unit

fun onCancel(teardown : TeardownLogic){
// 不能直接看到 TeardownLogic 內部的邏輯
}
複製代碼

typealias 容許縮短長泛型的名稱函數

typealias Doggos = List<Pet.GoodDoggo>

fun train(dogs: Doggos){ … }
複製代碼

固然也能夠縮短長類名性能

typealias AVD = AnimatedVectorDrawable
複製代碼

不過上面的場景使用 import alias 更合適ui

import android.graphics.drawable.AnimatedVectorDrawable as AVD
複製代碼

這種狀況下使用短命名並不能幫助咱們提升可讀性而且 IDE 會自動爲咱們補全類名

可是,若是須要區分來自不一樣包的同名類時,導入別名變得特別有用

import io.plaidapp.R as appR

import io.plaidapp.about.R
複製代碼

以上用例來自 Alter type with typealias

typealias 背後的實現

typealias D = Data

fun add(item: D) {

}

fun usage() {
    add(D("name"))
}
複製代碼

將 Data 聲明別名 D 並使用,Decompiled 爲 Java

能夠看到 typealias 並無聲明新的類型

您不該該依賴類型別名來進行編譯時類型檢查。 相反,您應該考慮使用 inline class

例如咱們的 play 方法須要傳遞 dog 的 id

fun play(dogId: Long)
複製代碼

在嘗試傳遞錯誤的 id 時,爲 Long 建立類型別名不會幫助咱們防止錯誤

typealias DogId = Long
fun play(dogId: DogId) { … }
fun usage() {
    val cat = Cat(1L)
    // 實際傳遞貓的 id ,可是能夠編譯經過
    play(cat.catId)
}
複製代碼

inline class 的使用

咱們知道聲明方法時能夠指定傳入參數的類型範圍

// 只容許傳入 layout 資源
public AppCompatActivity(@LayoutRes int contentLayoutId) {
    super(contentLayoutId);
}
複製代碼

可是若是咱們限制使用的不是 Android 的資源,而是 Dog Cat 這樣實體類的 id,咱們須要將其包裝到一個類中。這樣作的缺點就是須要付出額外的性能成本,本來可能只須要一個基本數據類型,如今使用時額外實例化了一個對象

Kotlin inline classes 容許您建立包裝類型而且沒有性能損耗。這是 Kotlin 1.3 中添加的實驗功能。inline class 必須有且僅有一個屬性。 在編譯時,將 inline class 實例替換爲其內的屬性(沒裝箱的基本數據類型),從而下降常規包裝類的性能損耗。 對於包裝對象是基本類型的狀況,基本數據類型包裝在 inline class 中會致使在運行時使用基本數據類型的值

內聯類的惟一做用是成爲類型的包裝,所以 Kotlin 對其作了不少限制:

  • 最多一個參數(類型不受限制)
  • 沒有 backing fields
  • 沒有初始化塊
  • 不能繼承類

inline class 能夠

  • 實現接口
  • 擁有屬性和方法
interface Id
inline class DoggoId(val id: Long) : Id {
    
    val stringId
    get() = id.toString()
    fun isValid()= id > 0L
}
複製代碼

typealias 看起來與 inline class 很像,可是 typealias 只是爲現有類型提供了別名,而 inline class 會建立新類型

inline class 背後的實現

讓咱們看一個簡單的 inline class

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

構造器

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)
  • 公開的構造函數 constructor_impl

當建立新的實例時將使用公開的構造函數

val myDoggoId = DoggoId(1L)
// decompiled
static final long myDoggoId = DoggoId.constructor-impl(1L);
複製代碼

當咱們嘗試在 Java 中建立 doggo 時,會報錯

DoggoId u = new DoggoId(1L);
// Error: DoggoId() in DoggoId cannot be applied to (long)
複製代碼

沒法在 Java 中實例化 inline class

參數化的構造函數是私有的,第二個構造函數在名稱中包含 -(在 Java 中爲無效字符)。 這意味着沒法在 Java 實例化 inline class

參數使用

這裏的 id 經過兩種方式使用

  • 做爲基本數據類型,經過 getId()
  • 經過 box_impl 建立 DoggoId 的實例化對象
public final long getId() {
      return this.id;
}

public static final DoggoId box_impl/* $FF was: box-impl*/(long v) {
      return new DoggoId(v);
}
複製代碼

若是在可使用基本數據類型的地方使用 inline classKotlin 編譯器將直接使用基本數據類型

fun walkDog(doggoId: DoggoId) {}

// decompiled Java code
public final void walkDog_Mu_n4VY(long doggoId) { }
複製代碼

當須要一個對象時,Kotlin 編譯器將使用基本數據類型的裝箱版本,從而每次都建立一個新的對象

下面咱們來看看須要裝箱的幾種狀況

可空對象

fun pet(doggoId: DoggoId?) {}

// decompiled Java code
public static final void pet_5ZN6hPs/* $FF was: pet-5ZN6hPs*/(@Nullable InlineDoggoId doggo) {}
複製代碼

只有引用數據類型才能爲 null ,所以須要裝箱

集合

val doggos = listOf(myDoggoId)
    
// decompiled Java code
doggos = CollectionsKt.listOf(DoggoId.box-impl(myDoggoId));

複製代碼
// CollectionsKt.listOf 
fun <T> listOf(element: T): List<T>
複製代碼

因爲該方法須要引用數據類型,所以須要裝箱

基類

fun handleId(id: Id) {}
fun myInterfaceUsage() {
    handleId(myDoggoId)
}
// decompiled Java code
public static final void myInterfaceUsage() {
    handleId(DoggoId.box-impl(myDoggoId));
}
複製代碼

這裏也須要裝箱

equals 檢查

Kotlin 編譯器會盡其所能使用沒裝箱的基本數據類型參數,所以,inline class 具備三種相等檢查的方式:1 個重寫 equals 和 2 個生成的方法

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;
   }

   // 重寫 equals 方法
   public boolean equals(Object var1) {
      return equals-impl(this.id, var1);
   }
}
複製代碼

doggo1.equals(doggo2)

equals 方法調用一個生成的方法:equals_impl(long,Object)。 因爲 equals 指望有一個對象,所以將對doggo2 值進行裝箱,可是將 doggo1 用做基本數據類型

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

doggo1 == doggo2

使用 == 等價於 DoggoId.equals-impl0(doggo1, doggo2)

所以使用 == doggo1 和 doggo2 均使用基本數據類型

doggo1 == 1L

若是 Kotlin 編譯器可以肯定 doggo1 是 long 類型,那麼這種相等性檢查有效。 可是,因爲 inline class 是類型安全的,所以,編譯器要作的第一件事是檢查這兩個對象的類型是否相同。 若是不相同,咱們會收到編譯器錯誤:Operator == can’t be applied to long and DoggoId

doggo1.equals(1L)

因爲 Kotlin 編譯器使用 equals 實現,所以須要一個 long 和一個 Object 進行相等檢查。 可是,因爲此方法的第一件事是檢查 Object 的類型,所以該相等性檢查將爲 false,由於 Object 不是 DoggoId

Zero-cost* abstractions in Kotlin 原文還介紹了在 Java 中使用 inline class 以及如何選擇是否使用 inline class,感興趣的小夥伴可移步原文查看,這裏不作介紹

關於我

我是 Fly_with24

相關文章
相關標籤/搜索