Kotlin | 淺談 reified 與泛型 那些事
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
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 ,一個三流開發,如果本文對你有所幫助,歡迎點贊支援,你的支援是我持續創作的最大鼓勵!
- 由淺入深,詳解 LiveData 的那些事
- Kotlin記憶體陷阱 | inline雖好,但別濫用
- 求知 | 聊聊Android資源載入的那些事 - 小試牛刀
- Kotlin | 關於協程異常處理,你想知道的都在這裡
- Kotlin | 淺談 reified 與泛型 那些事
- ViewPager 中 Fragment 狀態儲存的小知識
- 山川湖海 - Android無障礙代理的那些事
- Kotlin | 從執行緒到協程,你是否還存在 [同步] 上的使用疑問
- 哪怕不學Gradle,這些常見操作,你也值得掌握
- 淺談2022Android端技術趨勢,什麼 值得 學?
- Hi,這是一個普通Android開發的2021小結
- 開源 | 如何寫一個好用的 JetPack Compose 狀態頁元件
- 淺析 JetPack Compose 是如何安裝到 View 檢視上
- JetPack Compose 主題配色太少怎麼辦? 來設計自己的顏色系統吧
- 小知識 | 善用Mac自動化,少掉頭髮多喝茶
- 小知識 - Gradle7.0之後JitPack釋出元件需要注意的幾個問題
- 日常開發 - ViewPager2實現內部Item的動態滾動
- Flutter | 由Builder Widget而引發的思考
- 2020,懂得自己的平凡 | 掘金年度徵文
- Gradle | allprojects ,根 repositories 區別是什麼?