Kotlin 中的泛型

點此進入:從零快速構建APP系列目錄導圖
點此進入:UI編程系列目錄導圖
點此進入:四大組件系列目錄導圖
點此進入:數據網絡和線程系列目錄導圖

一、泛型基礎

泛型編程包括,在不指定代碼中使用到的確切類型的情況下來編寫算法。用這種方式,我們可以創建函數或者類型,唯一的區別只是它們使用的類型不同,提高代碼的可重用性。這種代碼單元就是我們所知道的泛型,它們存在於很多的語言之中,包括Java和Kotlin。

在Kotlin中,泛型甚至更加重要,因爲經常使用擴展函數將會成倍增加我們泛型使用頻率。儘管我們已經在本書中盲目地使用了泛型,但是泛型在任何語言中通常都是比較困難的一部分,所以我嘗試使用盡可能簡單的方式來講解它,這樣主要的思想也會足夠地清晰。

舉個例子,我們可以創建一個指定泛型類:

class TypedClass<T>(parameter: T) {
    val value: T = parameter
}

這個類現在可以使用任何的類型初始化,並且參數也會使用定義的類型,我們可以這麼做:

val t1 = TypedClass<String>("Hello World!")
val t2 = TypedClass<Int>(25)

但是Kotlin很簡單並且縮減了模版代碼,所以如果編譯器能夠推斷參數的類型,我們甚至也就不需要去指定它:

val t1 = TypedClass("Hello World!")
val t2 = TypedClass(25)
val t3 = TypedClass<String?>(null)

如第三個對象接收一個null引用,那仍然還是需要指定它的類型,因爲它不能去推斷出來。

我們可以像Java中那樣在定義中指定的方式來增加類型限制。比如,如果我們想限制上一個類中爲非null類型,我們只需要這麼做:

class TypedClass<T : Any>(parameter: T) { 
    val value: T = parameter
}

如果你再去編譯前面的代碼,你將看到t3現在會拋出一個錯誤。可null類型不再被允許了。但是限制明顯可以更加嚴厲。如果我們只希望Context的子類該怎麼做?很簡單:

class TypedClass<T : Context>(parameter: T) { 
    val value: T = parameter
}

現在所有繼承Context的類都可以在我們這個類中使用。其它的類型是不被允許的。

當然,可以使用函數中。我們可以相當簡單地構建泛型函數:

fun <T> typedFunction(item: T): List<T> {
    ...
}

二、泛型變體

這是真的是最難理解的部分之一。在Java中,當我們使用泛型的時候會出現問題。邏輯告訴我們List應該可以轉型爲List,因爲它有更弱的限制。但是我們來看下這個例子:

List<String> strList = new ArrayList<>();
List<Object> objList = strList;
objList.add(5);
String str = objList.get(0);

如果Java編譯器允許我們這麼做,我們可以增加一個Integer到Object List,但是它明顯會在某一時刻奔潰。這就是爲什麼語言中增加了通配符。通配符可以在限制這個問題中可以增加靈活性。

如果我們增加了? extends Object,我們使用了協變(covariance),它表示我們可以處理任何使用了類型,比Object更嚴格的對象,但是我們只有使用get操作時是安全的。如果我們想去拷貝一個Strings集合到Objects集合中,我們應該是允許的,對吧?然後,如果我們這樣:

List<String> strList = ...;
List<Object> objList = ...;
objList.addAll(strList);

這樣是可以的,因爲定義在Collection接口中的addAll()是這樣的:

List<String>
interface Collection<E> ... {
    void addAll(Collection<? extends E> items);
}

否則,沒有通配符,我們不會允許在這個方法中使用String List。相反地,當然會失敗。我們不能使用addAll()來增加一個Objects List到Strings List中。因爲我們只是用那個方法從collection中獲取元素,這是一個完美的協變(covariance)的例子。

另一方面,我們可以在對立面上發現逆變(contravariance)。按照集合的例子,如果我們想把傳過來的參數增加到集合中去,我們可以增加更加限制的類型到泛型集合中。比如,我們可以增加Strings到ObjectList:

void copyStrings(Collection<? super String> to, Collection<String> from) {
    to.addAll(from);
}

增加Strings到另一個集合中唯一的限制就是那個集合接收Strings或者父類。

但是通配符都有它的限制。通配符定義了使用場景變體(use-site variance),這意味着當我們使用它的時候需要聲明它。這表示每次我們聲明一個泛型變量時都會增加模版代碼。

讓我們看一個例子,使用我們之前相似的類:

class TypedClass<T> {
    public T doSomething(){
        ...
    }
}

這些代碼不會被編譯:

TypedClass<String> t1 = new TypedClass<>();
TypedClass<Object> t2 = t1;

儘管它的確沒有意義,因爲我們仍然保持了類中的所有的方法並且沒有任何損壞。我們需要指定的類型可以有一個更加靈活的定義。

TypedClass<String> t1 = new TypedClass<>();
TypedClass<? extends String> t2 = t1;

這會讓代碼更加難以理解,而且增加了一些額外的模版代碼。

另一方面,Kotlin通過內部聲明變體(declaration-site variance)可以使用更加容易的方式來處理。這表示當我們定義一個類或者接口的時候我們可以處理弱限制的場景,我們可以在其它地方直接使用它。

所以讓我們看看它在Kotlin中是怎麼工作的。相比冗長的通配符,Kotlin僅僅使用out來針對協變(covariance)和使用in來針對逆變(contravariance)。在這個例子中,當我們類產生的對象可以被保存到弱限制的變量中,我們使用協變。我們可以直接在類中定義聲明:

class TypedClass<out T>() {
    fun doSomething(): T {
        ...
    }
}

這就是所有我們需要的。現在,在Java中不能編譯的代碼在Kotlin中可以完美運行:

val t1 = TypedClass<String>()
val t2: TypedClass<Any> = t1

如果你已經使用了這些概念,我確信你可以很簡單地在Kotlin使用in和out。否則,你也只是需要一些聯繫和概念上的理解。

三、泛型例子

理論之後,我們轉移到一些實際功能上面,這會讓我們更加簡單地掌握它。爲了不重複發明輪子,我使用三個Kotlin標準庫中的三個函數。這些函數讓我們僅使用泛型的實現就可以做一些很棒的事情。它可以鼓舞你創建自己的函數。

1、let

let實在是一個簡單的函數,它可以被任何對象調用。它接收一個函數(接收一個對象,返回函數結果)作爲參數,作爲參數的函數返回的結果作爲整個函數的返回值。它在處理可null對象的時候是非常有用的,下面是它的定義:

inline fun <T, R> T.let(f: (T) -> R): R = f(this)

它使用了兩個泛型類型:T 和 R。第一個是被調用者定義的,它的類型被函數接收到。第二個是函數的返回值類型。

我們怎麼去使用它呢?你可能還記得當我們從數據源中獲取數據時,結果可能是null。如果不是null,則把結果映射到domain model並返回結果,否則直接返回null:

if (forecast != null) dataMapper.convertDayToDomain(forecast) else null

這代碼是非常醜陋的,我們不需要使用這種方式去處理可null對象。實際上如果我們使用let,都不需要if:

forecast?.let { dataMapper.convertDayToDomain(it) }

對虧?.操作符,let函數只會在forecast不是null的時候纔會執行。否則它會返回null。也就是我們想達到的效果。

2、with

本書中我們大量講了這個函數。with接收一個對象和一個函數,這個函數會作爲這個對象的擴展函數執行。這表示我們根據推斷可以在函數內使用this。

inline fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()

泛型在這裏也是以相同的方式運行:T代表接收類型,R代表結果。如你所見,函數通過f: T.() -> R聲明被定義成了擴展函數。這就是爲什麼我們可以調用receiver.f()。

通過這個app,我們有幾個例子:

fun convertFromDomain(forecast: ForecastList) = with(forecast) {
    val daily = dailyForecast map { convertDayFromDomain(id, it) }
    CityForecast(id, city, country, daily)
}

3、apply

它看起來於with很相似,但是是有點不同之處。apply可以避免創建builder的方式來使用,因爲對象調用的函數可以根據自己的需要來初始化自己,然後apply函數會返回它同一個對象:

inline fun <T> T.apply(f: T.() -> Unit): T { f(); return this }

這裏我們只需要一個泛型類型,因爲調用這個函數的對象也就是這個函數返回的對象。一個不錯的例子:

val textView = TextView(context).apply {
    text = "Hello"
    hint = "Hint"
    textColor = android.R.color.white
}

它創建了一個TextView,修改了一些屬性,然後賦值給一個變量。一切都很簡單,具有可讀性和堅固的語法。讓我們用在當前的代碼中。在ToolbarManager中,我們使用這種方式來創建導航drawable:

private fun createUpDrawable() = with(DrawerArrowDrawable(toolbar.ctx)) {
    progress = 1f
    this
}

使用with和返回this是非常清晰的,但是使用apply可以更加簡單:

private fun createUpDrawable() = DrawerArrowDrawable(toolbar.ctx).apply {
    progress = 1f
}

你可以在Kotlin for Android Developer代碼庫中查看這些小的優化。

點此進入:GitHub開源項目“愛閱”。“愛閱”專注於收集優質的文章、站點、教程,與大家共分享。下面是“愛閱”的效果圖:


聯繫方式:

簡書:WillFlow
CSDN:WillFlow
微信公衆號:WillFlow

微信公衆號:WillFlow

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章