1. 程式人生 > >[譯]Kotlin的獨門祕籍Reified實化型別引數(上篇)

[譯]Kotlin的獨門祕籍Reified實化型別引數(上篇)

翻譯說明:

原標題: Getting Real with Kotlin’s Reified Type Parameters

之前的Kotlin系列文章,歡迎檢視:

翻譯系列:

原創系列:

實戰系列:

簡述:(今天扯點不一樣的東西)

本篇已經是Kotlin泛型系列第三篇了,先來回顧下前面兩篇分別講的是泛型中的型別形參和型別實參以及什麼時候該使用型別形參約束。今天我們來說點Kotlin獨有的泛型特性,但是Java中是沒有的。那就是Kotlin中reified關鍵字修飾的泛型實化型別引數。再說這個之前我覺得有必要科普一下歷史背景以及為什麼Kotlin會出現實化型別引數。

歷史背景:

我們都知道Java中的泛型是在JDK1.5的版本引入的,可是集合Collection在JDK1.2版本中就引入的,我們現在所看到的List<T>,是在泛型出來後加入的,那麼JDK1.2之前就直接用List(java中俗稱原生態型別)表示。問題來了為了相容之前的版本Java採用所謂的偽泛型,偽泛型有個什麼特徵我想大家應該猜到那就是泛型擦除,就是泛型型別資訊在編譯期都會被抹掉,不管你是List<String>還是List<Float>在執行時他們都一樣,那都是List型別,泛型型別資訊已經被擦除了。當然泛型擦除也有它的好處,當然這不是這次討論的重點。偽泛型對應的就是真泛型,如果熟悉C#就知道,它是真泛型,不會存在型別擦除情況,具體可以自己去了解下。

時勢造英雄Kotlin登場:

顯然我們知道,泛型擦除在一些開發場景下是有很大影響,使用起來非常不便,Kotlin這門新的語言不像Java一樣有太多的歷史負擔,它就像是全域性者一樣,看著Java之前的坑走過來的,於是乎它想來填一填。有的人可能會問了那它是不是像C#那樣採用真泛型,答案不是。我們都知道Kotlin力求做到與Java百分百的互操作性,所以Kotlin妥協了還是採用偽泛型,所以它和Java一樣依然會存在泛型擦除問題。但是很幸運地是Kotlin偷偷給我們開了一個後門那就是今天的主角Reified實化型別引數,它可以保證執行時依然能拿到泛型具體實際型別。Reified實化型別引數已經大量運用在Kotlin的anko中,關於這塊後面部落格會細講。

進入正題(開始翻譯)

讓我們來想一想在Kotlin中你可以類名來做些什麼-想想你在原始碼中編寫類名的所有場景。我想到了列舉以下15種情景,沒想全可能會有漏一些。讓我們一起來看下吧…

  • 1、定義一個成員屬性
private val thing: Thing
  • 2、函式引數
fun doSomething(thing: Thing) {}
  • 3、型別實參
val list = listOf<Thing>()
  • 4、型別形參約束
class Item<T : Thing>
  • 5、強制型別轉換
something as Thing
  • 6、定義一個類
open class Thing {}
  • 7、繼承一個類
class Other : Thing()
  • 8、匯入一個類
import com.example.Thing
  • 9、給類名定義typealias別名
typealias Thingy = Thing
  • 10、構建一個物件
val thing = Thing()
  • 11、捕獲一個錯誤型別
catch (e: ExceptionalThing)
  • 12、呼叫靜態方法
Thing.doStaticStuff()
  • 13、定義函式引用
val function = Thing::doSomething
  • 14、型別比較
something is Thing
  • 15、獲得類的一個Class物件:
val clazz = Thing::class.java

大問題來了

現在,最大的問題是:

在上面哪些案例中我們應該使用泛型型別引數引用而不是真實的類名?

換句話說,上面15種案例中哪些我們可以用型別引數比如 T去替代Thing類呢?

你在哪些地方可以引用型別引數?

你想到了什麼?
這是我能想到的 - 上面的案例1-5都可以採用型別引數。讓我們一起展示所有五個:

class GenericThing<T>(constructorArg: T) {
    // 1. Define a member property 定義一個成員屬性
    private val thing: T = constructorArg

    // 2. Define the type of a function argument 定義函式引數
    fun doSomething(thing: T) = println(thing)

    // 3. Use as a type argument 定義泛型型別實參
    fun emptyList() = listOf<T>()

    // 4. Use as a type parameter constraint, and... 使用作為型別形參約束
    // 5. Cast to the type (produces "unchecked cast" warning) 強制型別轉換
    fun <U : T> castIt(): T = thing as U
}

你在哪些地方可以不引用型別引數?

引用型別引數適用於案例1-5。但是對於案例6-15,如果我們要在(例如,Thing或ExceptionalThing)的地方替換型別引數(例如T),則最終會出現編譯器錯誤。

  • 在某些情況下,使用型別引數是沒有意義的。例如,在案例6中我們正在定義一個類 - 根據其型別引數之一定義一個類有什麼意義呢?
  • 在其他情況下,Java和Kotlin通過反射提供一些變通的方法,例如構建物件(案例10)
    但在某些情況下,能夠使用型別引數肯定會很好。特別是,案例14和15–比較型別和分配類物件 - 在某些情況下會非常方便。

Reified型別實化引數介紹

Java限制了哪些型別是reifiable - reifiable也就意味著它們“在執行時完全可用”(具體可查閱:Java SE specs on reifiable types),泛型型別引數通常在編譯期間被擦除,但是在Kotlin中的reified型別引數的情況下, 由於底層語法下一些巧妙的技巧,讓執行時也能準確拿到泛型引數型別資訊。

Reified型別引數僅適用於函式(或具有get()函式的擴充套件屬性),並且僅適用於宣告為inline內聯的函式。這是一個例子:

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

當您將函式標記為inline時,編譯器會把實現行內函數的位元組碼插入到每次呼叫發生的地方。這就是reified型別的工作原理 - 具體實際型別在呼叫地方是已經知道的,因此在呼叫x.isInstanceOf<String>()有效地把x編譯為String.

reified型別實化引數經常被使用到的幾個地方

上面的案例15是許多Kotlin開發人員最喜歡的案例。假設我們有一個User類,以及我們想要讀取的JSON字串

data class User(val first: String, val last: String)

val json = """{
      "first": "Sherlock",
      "last": "Holmes"
    }""

在Java序列化庫(如Gson)中,當您想要反序列化該JSON字串時,您最終必須將Class物件作為引數傳遞,以便Gson知道您想要的型別。

User user = new Gson().fromJson(getJson(), User.class);

現在,讓我們一起展示reified型別實化引數的魔法 我們將建立一個非常輕量級的擴充套件函式來包裝Gson方法:

inline fun <reified T> Gson.fromJson(json: String) = 
        fromJson(json, T::class.java) 

現在,在我們的Kotlin程式碼中,我們可以反序列化JSON字串,甚至根本不需要傳遞型別資訊!

val user: User = Gson().fromJson(json)

Kotlin根據它的用法推斷出型別 - 因為我們將它分配給User型別的變數,Kotlin使用它作為fromJson()的型別引數。或者,您可以使型別推斷其他的類:

val user = Gson().fromJson<User>(json)

在這種情況下,從傳遞給fromJson()的型別引數推斷出user型別。

譯者有話說

這篇文章屬於reified-type-parameter實化型別引數的上篇,算是一個簡單開頭介紹怎麼使用reified,然後前面花了一些篇幅闡述了開發中什麼情況下可以使用泛型,感覺這點應該更實用吧,下篇譯文將會比較深入reified講解實化型別引數,敬請期待。

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~