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

  • 特定條件和情況
    這篇博客描述了一個 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ò)展閱讀:

如果我們的 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)資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末获搏,一起剝皮案震驚了整個濱河市赖条,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌常熙,老刑警劉巖纬乍,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異裸卫,居然都是意外死亡仿贬,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門墓贿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茧泪,“玉大人,你說我怎么就攤上這事聋袋《游埃” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵幽勒,是天一觀的道長嗜侮。 經(jīng)常有香客問我,道長啥容,這世上最難降的妖魔是什么锈颗? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮干毅,結(jié)果婚禮上宜猜,老公的妹妹穿的比我還像新娘。我一直安慰自己硝逢,他們只是感情好姨拥,可當(dāng)我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布绅喉。 她就那樣靜靜地躺著,像睡著了一般叫乌。 火紅的嫁衣襯著肌膚如雪柴罐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天憨奸,我揣著相機(jī)與錄音革屠,去河邊找鬼。 笑死排宰,一個胖子當(dāng)著我的面吹牛似芝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播板甘,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼党瓮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了盐类?” 一聲冷哼從身側(cè)響起寞奸,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎在跳,沒想到半個月后枪萄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡猫妙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年瓷翻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吐咳。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡逻悠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出韭脊,到底是詐尸還是另有隱情童谒,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布沪羔,位于F島的核電站饥伊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蔫饰。R本人自食惡果不足惜琅豆,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望篓吁。 院中可真熱鬧茫因,春花似錦、人聲如沸杖剪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至洛巢,卻和暖如春括袒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背稿茉。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工锹锰, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人漓库。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓恃慧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親米苹。 傳聞我的和親對象是個殘疾皇子糕伐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,611評論 2 353