Kotlin泛型與DSL重點記錄
泛型
kotlin的泛型語法與java類似,比如宣告一個泛型類:
interface List<T>{ operator fun get(index:Int):T }
型別引數約束
類似於java中的entends
關鍵字,在kotlin中可以把冒號放在型別引數名稱之後,作為型別形參上界的型別緊隨其後:
fun <T : Number> List<T>.sum():T
在kotlin中還可以為一個型別引數指定多個約束,比如:
fun <T> ensureTraillingPeriod(seq:T) where T:CharSequence, T:Appendable{ if(!seq.startWith('.')){ seq.append('.') } }
由於kotlin中Any
和Any?
是不同的型別,因此T
和T?
定義型別引數時意義也是不一樣的。並且在kotlin中,如果你沒有指定型別形參的上界,預設使用Any?
作為上界。
<T : Any> ` 這個就確保了T永遠是非空型別。
泛型擦除
java中的泛型是通過型別擦除
實現的,就是說泛型類例項的型別實參在執行時是不保留的。kotlin的泛型在執行時也被擦除了。即下面這個程式碼是編譯不過的:
if(value is List<String>)//編譯錯誤,這是因為在執行時,是沒有String這個型別實參的。
泛型擦除的好處是可以降低應用程式使用的記憶體,因為儲存在記憶體中的型別資訊更少。
看下面這個函式,來理解一下型別擦除
:
//這個函式式可以正常通過編譯的 fun printSum(c:Collection<*>){ // * 表示不確定是哪種型別 val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected") println(intList.sum()) } printSum(setOf(1,2,3))//IllegalArgumentException -> Set不是List printSum(listOf("a", "b", "c")) //ClassCastException -> String轉Int失敗
對於第二個字串列表的呼叫,這是因為沒有辦法判斷實參是不是一個List<Int>
(執行時被擦除了),因此型別轉換會成功,接下來呼叫inList.sum()
時,由於sum
函式會把list中的元素轉換為Int
型別,因此就會報
ClassCastException.
下面這個函式就是ok的,這是因為編譯器是很智慧的,在編譯階段就能杜絕型別轉換失敗的問題:
fun printSum(c:Collection<Int>){ if(c is List<Int>) print(c.sum()) }
宣告帶實化型別引數的函式
由於泛型擦除,在呼叫泛型函式的時候,在函式體中你不能決定呼叫它用的型別實參,比如:
fun <T> isA(value:Any) = value is T//編譯失敗
但是行內函數+reified
可以解決這個問題,由於編譯器在編譯行內函數時,會把每一次函式呼叫都轉換為函式的實際程式碼實現,所有編譯器知道每次呼叫中用作型別實參的確切型別,因此,編譯器可以生成引用作為型別實參的具體類的位元組碼。
把上面程式碼這樣一改就可以通過編譯:
inline fun <reified T> isA(value:Any) = value is T -> print(isA<String>("q"))true -> print(isA<Int>("a"))false
但需要注意,這種函式不能在java中呼叫。
一個簡單的簡化startActivity
的例子:
inline fun <reified T : Activity> Context.startActivity(){ startActivity(Intent(this, T::class.java)) } -> startActivity<DetailActivity>()
kotlin推薦我們在下列場景時使用實化型別引數:
- 用在型別檢查和型別轉換中(is/!is/as/as?)
- 使用kotlin反射API (::class)
- 獲取相應的java.lang.Class (::class.java)
- 作為呼叫其他函式的型別實參
協變(out)與逆變(in)
在kotlin的泛型的概念裡,如果A是B的子型別,那麼List<A>就是List<B>的子型別,這樣的類或者介面就是協變的。
假如有下面這個函式:
open class Animal class Cat : Animal() class Herd<T : Animal> fun flap(animals: Herd<Animal>) { //... } flap(animals) flap(cats) //編譯報錯, 這是因為 Herd<Cat> 並不是 Herd<Animal>的子型別。
我們可以在宣告泛型時使用out
關鍵字來解決上面這個問題,class Herd<out T : Animal>
, 這樣就說明Herd
這個類是協變的。
out關鍵字要求所有使用T的方法只能把T放在out位置(返回值)而不能放在in位置(引數)上。如果對一個類使用的out,說明這個類只能生產型別T(返回)而不能消費他們(作為引數)。即型別T上的out有下面兩層含義:
- 子型別化會被保留
- T只能用在out位置(返回值)
什麼是out位置,什麼是in位置
比如一個類,它聲明瞭一個型別引數T幷包含了一個使用T的函式,如果函式時把T當成返回型別,我們說它在out位置,這種情況下,該函式生產型別為T的值,如果T用在函式引數的型別,它就在in位置。這樣的函式消費型別為T的值。
為什麼協變的泛型只能放在out位置呢?
假如可以放在in
位置, 還是上面Herd
的例子,如果animals 為Herd<Cat>
, 假如在flap
函式中,把Herd<Cat>
變為了Herd<Dog>
並返回,那麼使用者接下來就可能報錯,因此,out
約束保證了子型別關係的安全性。
構造方法的引數既不在in位置,也不再out位置
看一個帶out投影型別引數的資料拷貝函式:
//這裡可以給型別的用法加上"out"關鍵字:沒有使用那些T用在"in"位置的方法 fun <T> copyData(source:MutableList<out T>, des:MutableList<T>){ for(item in source){ des.add(item) } } //如果不寫out,這裡是無法返回成功的 fun getAnimal(type:String):List<out Animal>{ return List<Cat>() }
- 與協變相反的逆變in
逆變的概念可以被看成是協變的映象,具體是這樣定義的:如果B是A的子型別,那麼Consumer<A>就是Consumer<B>的子型別,型別A和B交換了位置,所有說子型別化被反轉了。
星號投影:使用"*"代替型別引數
一個包含未知型別的元素的列表可以用List<*>表示。
MutableList< * >與MutableList<Any?>是不一樣的,MutableList<Any?>包含的是任意型別的元素,而 MutableList< * >包含的是某種特定型別的元素列表,但不知道是哪個型別。
舉個例子:
val list1 = listOf('a', 2, "qq") val unknowTypeList:MutableList<*> = list1
在這個例子中編譯器會把 MutableList<*>當成 out投影型別,即你只能在這個列表上讀取元素,而不能寫入元素。
星號投影的語法很簡潔,但只能用在對泛型型別實參的確切值不感興趣的地方:只是使用生產值的方法,而不關心那些值的型別
kotlin DSL
這裡不去看kotlin中的DSL的具體內容,我們只看一個組成kotlin DSL的非常基本的特性:
DSL中帶接收者的lambda
先來看一個普通的例子:
fun buildString(builderAction: (StringBuilder) -> Unit):String{ val sb = StringBuilder() builderAction(sb) return sb.toString } val s = buildString{ it.append("a") it.append("b") }
上面是定義了一個接收函式型別的函式builderAction
, 上面我們直接傳遞了一個lambda表示式,並且使用kotlin提供的it
來訪問唯一的StringBuilder
引數。
但是有沒有感覺到,每次都使用it
很麻煩? 可不可以直接呼叫append方法,就是作用在StringBuilder
引數上?其實這個kotlin是支援的,只需要將lambda轉換成帶接收者的lambda,我們來改寫上面的實現:
fun buildString(builderAction:StringBuilder.()->Unit):String{ val sb = StringBuilder() sb.builderAction()// -> builderAction(sb)也可以這樣呼叫 return sb.toString() } val s = builderAction{ append("a") append("b") }
可以看到上面我們使用:StringBuilder().->Unit
代替了(StringBuilder) ->Unit
,這個特殊的型別就叫做接受者型別
或 這是一個擴充套件函式型別
: 接收者型別是String,沒有引數,返回值是Unit。
一個擴充套件函式型別描述了一個尅被當做擴充套件函式呼叫的程式碼塊(對於擴充套件函式,我們可以直接在函式中,呼叫擴充套件類的屬性或方法,這個是我們已知的)。
我們知道apply
函式就是帶接收者的函式,我們看看這個函式是怎麼宣告的:
inline fun <T> T.apply(block:T.()->Unit):T{ block() return this } inline fun <T, R> with(reveiver:T, block:T.()->R):R = reveiver.block()