Kotlin的獨門秘籍Reified實化類型參數(shù)(下篇)

Kotlin系列文章,歡迎查看:

原創(chuàng)系列:

翻譯系列:

實戰(zhàn)系列:

簡述:
今天我們開始接著原創(chuàng)系列文章,首先說下為什么不把這篇作為翻譯篇呢?我看了下作者的原文,里面講到的,這篇博客都會有所涉及。這篇文章將會帶你全部弄懂Kotlin泛型中的reified實化類型參數(shù),包括它的基本使用、源碼原理、以及使用場景。有了上篇文章的介紹,相信大家對kotlin的reified實化類型參數(shù)有了一定認識和了解。那么這篇文章將會更加完整地梳理Kotlin的reified實化類型參數(shù)的原理和使用。廢話不多說,直接來看一波章節(jié)導(dǎo)圖:

image

一、泛型類型擦除

通過上篇文章我們知道了JVM中的泛型一般是通過類型擦除實現(xiàn)的,也就是說泛型類實例的類型實參在編譯時被擦除,在運行時是不會被保留的。基于這樣實現(xiàn)的做法是有歷史原因的,最大的原因之一是為了兼容JDK1.5之前的版本,當然泛型類型擦除也是有好處的,在運行時丟棄了一些類型實參的信息,對于內(nèi)存占用也會減少很多。正因為泛型類型擦除原因在業(yè)界Java的泛型又稱偽泛型。因為編譯后所有泛型的類型實參類型都會被替換Object類型或者泛型類型形參指定上界約束類的類型。例如:
List<Float>、List<String>、List<Student>在JVM運行時Float、String、Student都被替換成Object類型,如果是泛型定義是List<T extends Student>那么運行時T被替換成Student類型,具體可以通過反射Erasure類可看出。

雖然Kotlin沒有和Java一樣需要兼容舊版本的歷史原因,但是由于Kotlin編譯器編譯后出來的class也是要運行在和Java相同的JVM上的,JVM的泛型一般都是通過泛型擦除,所以Kotlin始終還是邁不過泛型擦除的坎。但是Kotlin是一門有追求的語言不想再被C#那樣噴Java說什么泛型集合連自己的類型實參都不知道,所以Kotlin借助inline內(nèi)聯(lián)函數(shù)玩了個小魔法。

二、泛型擦除會帶來什么影響?

泛型擦除會帶來什么影響,這里以Kotlin舉例,因為Java遇到的問題,Kotlin同樣需要面對。來看個例子

fun main(args: Array<String>) {
    val list1: List<Int> = listOf(1,2,3,4)
    val list2: List<String> = listOf("a","b","c","d")
    println(list1)
    println(list2)
}

上面兩個集合分別存儲了Int類型的元素和String類型的元素,但是在編譯后的class文件中的他們被替換成了List原生類型一起來看下反編譯后的java代碼

@Metadata(
   mv = {1, 1, 11},
   bv = {1, 0, 2},
   k = 2,
   d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005¨\u0006\u0006"},
   d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "Lambda_main"}
)
public final class GenericKtKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      List list1 = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4});//List原生類型
      List list2 = CollectionsKt.listOf(new String[]{"a", "b", "c", "d"});//List原生類型
      System.out.println(list1);
      System.out.println(list2);
   }
}

我們看到編譯后listOf函數(shù)接收的是Object類型,不再是具體的String和Int類型了。


image

1、類型檢查問題:

Kotlin中的is類型檢查,一般情況不能檢測類型實參中的類型(注意是一般情況,后面特殊情況會細講),類似下面。

if(value is List<String>){...}//一般情況下這樣的代碼不會被編譯通過

分析: 盡管我們在運行時能夠確定value是一個List集合,但是卻無法獲得該集合中存儲的是哪種類型的數(shù)據(jù)元素,這就是因為泛型類的類型實參類型被擦除,被Object類型代替或上界形參約束類型代替。但是如何去正確檢查value是否List呢?請看以下解決辦法

Java中的解決辦法: 針對上述的問題,Java有個很直接解決方式,那就是使用List原生類型。

if(value is List){...}

Kotlin中的解決辦法:
我們都知道Kotlin不支持類似Java的原生類型,所有的泛型類都需要顯示指定類型實參的類型,對于上述問題,kotlin中可以借助星投影List<*>(關(guān)于星投影后續(xù)會詳細講解)來解決,目前你暫且認為它是擁有未知類型實參的泛型類型,它的作用類似Java中的List<?>通配符。

if(value is List<*>){...}

特殊情況: 我們說is檢查一般不能檢測類型實參,但是有種特殊情況那就是Kotlin的編譯器智能推導(dǎo)(不得不佩服Kotlin編譯器的智能)

fun printNumberList(collection: Collection<String>) {
    if(collection is List<String>){...} //在這里這樣寫法是合法的。
}

分析: Kotlin編譯器能夠根據(jù)當前作用域上下文智能推導(dǎo)出類型實參的類型,因為collection函數(shù)參數(shù)的泛型類的類型實參就是String,所以上述例子的類型實參只能是String,如果寫成其他的類型還會報錯呢。

2、類型轉(zhuǎn)換問題:

在Kotlin中我們使用as或者as?來進行類型轉(zhuǎn)換,注意在使用as轉(zhuǎn)換時,仍然可以使用一般的泛型類型。只有該泛型類的基礎(chǔ)類型是正確的即使是類型實參錯誤也能正常編譯通過,但是會拋出一個警告。一起來看個例子

image
package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf(1, 2, 3, 4, 5))//傳入List<Int>類型的數(shù)據(jù)
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>//強轉(zhuǎn)成List<Int>
    println(numberList)
}

運行輸出

image
package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf("a", "b", "c", "d"))//傳入List<String>類型的數(shù)據(jù)
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    //這里強轉(zhuǎn)成List<Int>,并不會報錯,輸出正常,
    //但是需要注意不能默認把類型實參當做Int來操作,因為擦除無法確定當前類型實參,否則有可能出現(xiàn)運行時異常
    println(numberList)
}

運行輸出

image

如果我們把調(diào)用地方改成setOf(1,2,3,4,5)

fun main(args: Array<String>) {
    printNumberList(setOf(1, 2, 3, 4, 5))
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    println(numberList)
}

運行輸出

image

分析: 仔細想下,得到這樣的結(jié)果也很正常,我們知道泛型的類型實參雖然在編譯期被擦除,泛型類的基礎(chǔ)類型不受其影響。雖然不知道List集合存儲的具體元素類型,但是肯定能知道這是個List類型集合不是Set類型的集合,所以后者肯定會拋異常。至于前者因為在運行時無法確定類型實參,但是可以確定基礎(chǔ)類型。所以只要基礎(chǔ)類型匹配,而類型實參無法確定有可能匹配有可能不匹配,Kotlin編譯采用拋出一個警告的處理。

注意: 不建議這樣的寫法容易存在安全隱患,由于編譯器只給了個警告,并沒有卡死后路。一旦后面默認把它當做強轉(zhuǎn)的類型實參來操作,而調(diào)用方傳入的是基礎(chǔ)類型匹配而類型實參不匹配就會出問題。

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf("a", "b", "c", "d"))
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    println(numberList.sum())
}

運行輸出

image

三、什么是reified實化類型參數(shù)函數(shù)?

通過以上我們知道Kotlin和Java同樣存在泛型類型擦除的問題,但是Kotlin作為一門現(xiàn)代編程語言,他知道Java擦除所帶來的問題,所以開了一扇后門,就是通過inline函數(shù)保證使得泛型類的類型實參在運行時能夠保留,這樣的操作Kotlin中把它稱為實化,對應(yīng)需要使用reified關(guān)鍵字。

1、滿足實化類型參數(shù)函數(shù)的必要條件

  • 必須是inline內(nèi)聯(lián)函數(shù),使用inline關(guān)鍵字修飾
  • 泛型類定義泛型形參時必須使用reified關(guān)鍵字修飾

2、帶實化類型參數(shù)的函數(shù)基本定義

inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T 

對于以上例子,我們可以說類型形參T是泛型函數(shù)isInstanceOf的實化類型參數(shù)。

3、關(guān)于inline函數(shù)補充一點

我們對inline函數(shù)應(yīng)該不陌生,使用它最大一個好處就是函數(shù)調(diào)用的性能優(yōu)化和提升,但是需要注意這里使用inline函數(shù)并不是因為性能的問題,而是另外一個好處它能是泛型函數(shù)類型實參進行實化,在運行時能拿到類型實參的信息。至于它是怎么實化的可以接著往下看

四、實化類型參數(shù)函數(shù)的背后原理以及反編譯分析

我們知道類型實化參數(shù)實際上就是Kotlin變得的一個語法魔術(shù),那么現(xiàn)在是時候揭開魔術(shù)神秘的面紗了。說實在的這個魔術(shù)能實現(xiàn)關(guān)鍵得益于內(nèi)聯(lián)函數(shù),沒有內(nèi)聯(lián)函數(shù)那么這個魔術(shù)就失效了。

1、原理描述

我們都知道內(nèi)聯(lián)函數(shù)的原理,編譯器把實現(xiàn)內(nèi)聯(lián)函數(shù)的字節(jié)碼動態(tài)插入到每次的調(diào)用點。那么實化的原理正是基于這個機制,每次調(diào)用帶實化類型參數(shù)的函數(shù)時,編譯器都知道此次調(diào)用中作為泛型類型實參的具體類型。所以編譯器只要在每次調(diào)用時生成對應(yīng)不同類型實參調(diào)用的字節(jié)碼插入到調(diào)用點即可。 總之一句話很簡單,就是帶實化參數(shù)的函數(shù)每次調(diào)用都生成不同類型實參的字節(jié)碼,動態(tài)插入到調(diào)用點。由于生成的字節(jié)碼的類型實參引用了具體的類型,而不是類型參數(shù)所以不會存在擦除問題。

2、reified的例子

帶實化類型參數(shù)的函數(shù)被廣泛應(yīng)用于Kotlin開發(fā),特別是在一些Kotlin的官方庫中,下面就用Anko庫(簡化Android的開發(fā)kotlin官方庫)中一個精簡版的startActivity函數(shù)

inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)

通過以上例子可看出定義了一個實化類型參數(shù)T,并且它有類型形參上界約束Activity,它可以直接將實化類型參數(shù)T當做普通類型使用

3、代碼反編譯分析

為了好反編譯分析單獨把庫中的那個函數(shù)拷出來取了startActivityKt名字便于分析。

class SplashActivity : BizActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()//只需這樣就直接啟動了AccountActivity了,指明了類型形參上界約束Activity
    }
}

inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)

編譯后關(guān)鍵代碼

//函數(shù)定義反編譯
 private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      Intrinsics.reifiedOperationMarker(4, "T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);//注意點一: 由于泛型擦除的影響,編譯后原來傳入類型實參AccountActivity被它形參上界約束Activity替換了,所以這里證明了我們之前的分析。
   }
//函數(shù)調(diào)用點反編譯
protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);
      //注意點二: 可以看到這里函數(shù)調(diào)用并不是簡單函數(shù)調(diào)用,而是根據(jù)此次調(diào)用明確的類型實參AccountActivity.class替換定義處的Activity.class,然后生成新的字節(jié)碼插入到調(diào)用點。
}

讓我們稍微在函數(shù)加點輸出就會更加清晰

class SplashActivity : BizActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()
    }
}

inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) {
    println("call before")
    AnkoInternals.internalStartActivity(this, T::class.java, params)
    println("call after")
}

反編譯后

private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      String var3 = "call before";
      System.out.println(var3);
      Intrinsics.reifiedOperationMarker(4, "T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);
      var3 = "call after";
      System.out.println(var3);
   }

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      String var4 = "call before";
      System.out.println(var4);
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);//替換成確切的類型實參AccountActivity.class
      var4 = "call after";
      System.out.println(var4);
   }
   

五、實化類型參數(shù)函數(shù)的使用限制

這里說的使用限制主要有兩點:

1、Java調(diào)用Kotlin中的實化類型參數(shù)函數(shù)限制

明確回答Kotlin中的實化類型參數(shù)函數(shù)不能在Java中的調(diào)用,我們可以簡單的分析下,首先Kotlin的實化類型參數(shù)函數(shù)主要得益于inline函數(shù)的內(nèi)聯(lián)功能,但是Java可以調(diào)用普通的內(nèi)聯(lián)函數(shù)但是失去了內(nèi)聯(lián)功能,失去內(nèi)聯(lián)功能也就意味實化操作也就化為泡影。故重申一次Kotlin中的實化類型參數(shù)函數(shù)不能在Java中的調(diào)用

2、Kotlin實化類型參數(shù)函數(shù)的使用限制

  • 不能使用非實化類型形參作為類型實參調(diào)用帶實化類型參數(shù)的函數(shù)
  • 不能使用實化類型參數(shù)創(chuàng)建該類型參數(shù)的實例對象
  • 不能調(diào)用實化類型參數(shù)的伴生對象方法
  • reified關(guān)鍵字只能標記實化類型參數(shù)的內(nèi)聯(lián)函數(shù),不能作用與類和屬性。
qrcode_for_gh_109398d5e616_430.jpg

歡迎關(guān)注Kotlin開發(fā)者聯(lián)盟,這里有最新Kotlin技術(shù)文章,每周會不定期翻譯一篇Kotlin國外技術(shù)文章。如果你也喜歡Kotlin,歡迎加入我們~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容