Kotlin | 淺談 reified 與泛型 那些事

語言: CN / TW / HK

theme: orange highlight: androidstudio


“我報名參加金石計劃1期挑戰——瓜分10萬獎池,這是我的第1篇文章,點選檢視活動詳情

背景

在業務中,或者要寫某個技術元件時,我們無可避免會經常使用到 泛型 ,從而讓程式碼更具複用性與健壯性。 但相應的,由於Java泛型存在 型別擦除 的實現機制,所以某些情況下就會顯得力不從心。

而在 Kotlin 中,由於最終也會被編譯為 java位元組碼 ,所以無可避免也存在著上述問題🙂。

什麼是型別擦除?

如下例所示:

kotlin Class c1=new ArrayList<Integer>().getClass(); Class c2=new ArrayList<String>().getClass(); // 輸出為true System.out.println(c1==c2);

上述輸出結果為什麼會true呢?

因為泛型從底層上說是一種語法糖,它只存在於 編譯期 。在程式碼執行期間,jvm會將泛型的相關資訊擦除,成功編譯後的 class檔案 不會包含任何泛型資訊。所以在執行時,c1與c2儲存的型別都為Object,而 Integer 與 String 已經被擦除。

正是由於型別被擦除,所以就導致一些相關問題,比如不安全的型別轉換,模版方法的增多等。

reified

為了解決上述型別擦除問題,以及更好的使用體驗。Kotlin 中存在名為 reified 的關鍵字,它可以被作用於函式上, 以此做到型別擦除後的再生,便於開發者優雅的使用泛型以及獲取方法的泛型型別。

Java 中,如果我們要獲取函式的泛型型別,一般會通過給函式中傳遞型別引數的方式,如下所示:

java public <T extends Activity> void startActivity(Context context, Class<T> c) { context.startActivity(new Intent(context, c)); }

如果上述程式碼使用 kotlin 來寫呢?如下所示:

kotlin inline fun <reified C : Activity> Context.startActivityKtx() { startActivity(Intent(this, C::class.java)) }

我們利用 擴充套件函式 + reified 關鍵字的方式,減少了模版程式碼,增強了使用體驗。

從而讓本該在編譯階段被擦除的Activity型別,能夠在執行時獲取到。

但需要注意的是,reified 關鍵字必須和 inline 關鍵字一起使用(下面會提到為什麼)。

inline 關鍵字是什麼呢?

簡單理解為:當一個函式被標記為 inline 時,kotlin編譯器 會在所有呼叫這個函式的位置,將方法函式替換為具體的函式體。

解析

通過檢視 kotlin 位元組碼,我們可以得知 reified 的底層實現。

例如下面示例與其對應的位元組碼:

```kotlin inline fun Context.toAct() { startActivity(Intent(this, C::class.java)) }

fun test(context: Context) { context.toAct() } ```

```java // $FF: synthetic method public static final void toAct(Context $this$toAct) { int $i$f$toAct = 0; Intrinsics.checkNotNullParameter($this$toAct, "$this$toAct"); Intrinsics.reifiedOperationMarker(4, "C"); $this$toAct.startActivity(new Intent($this$toAct, Activity.class)); }

public static final void test(@NotNull Context context) { Intrinsics.checkNotNullParameter(context, "context"); int $i$f$toAct = false; context.startActivity(new Intent(context, MainActivity.class)); } ```

我們在 test() 方法中呼叫toAct(),不難發現,toAct()的邏輯已經被移動到了 test() 中,而我們的泛型型別也被替換為實際使用的型別,從而我們可以在方法函式中直接獲取相應的泛型型別。

這也就是為什麼 reified 必須要增加 inline ,因為其必須內聯才能知道具體型別,從而將我們的實際泛型型別更新到具體的呼叫程式碼中,從而完成泛型型別 再生

小提示

Java中無法呼叫

需要注意的是,reified 無法在java中進行呼叫,為什麼呢?

因為 Java 並沒有內聯的特性,我們使用的 inline 方法在 Java 中會被當做普通方法,而 reified 正是需要內聯才可以保證泛型再生,所以自然無法呼叫。

從原始碼上來說,對於reified 關鍵字的方法,相應的位元組碼生成時會增加 // $FF: synthetic method 的標記。

如下示例所示:

kotlin inline fun <reified C : Activity> Context.toAct() { ... }

kotlin // $FF: synthetic method public static final void toAct(Context $this$toAct) { ... }

// $FF: synthetic method

如果你經常寫元件,肯定見過這段註釋。你可以理解這只是一個標記,其作用為告訴編譯器 禁止java程式碼在編譯期訪問該方法

比如我們在寫 kotlin 元件,而且要同時滿足 java 呼叫時,經常會免不了使用 internal ,即 模組可見 。但相應的,該關鍵字修飾的方法或者欄位在Java中卻依然可以被呼叫,甚是讓java呼叫者費解與不優雅。所以相應的,對於方法,我們可以增加 @JvmSynthetic ,從而避免java程式碼編譯期呼叫。而 reified 也是正是採用了該思路。

當然也可以採用 @JvmName(name=" xxx") 等方式避免java呼叫,但其並不是很優雅。

效能方面

使用 reified 不會帶來任何效能損失,相反還會增強效能(源於inline)。

之所以這裡還要提到,主要是因為如果 行內函數過於複雜,則可能會導致效能問題。所以 reified 的使用其實也需要遵循行內函數的最佳實踐。

如果檢視Kotlin的標準行內函數,你會發現,程式碼行數大部分只有1-3行,因為inline會增加程式碼量的生成,行內函數越複雜,相應的程式碼量也越高,具體的使用方面,可以參見這篇 Kotlin Vocabulary | 行內函數的原理與應用

參考

Kotlin Vocabulary | Reified: 型別擦除後再生計劃

關於我

我是 Petterp ,一個三流開發,如果本文對你有所幫助,歡迎點贊支援,你的支援是我持續創作的最大鼓勵!