型態的緊箍咒

我們在前面曾經見過的泛型......

我們講到集合的時候,曾經看過泛型例如:Array<Int>List<String>這種。在介紹標準函數的時候,也有看到過:

public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

那泛型到底是什麼呢?

利用約束讓程式可以做更多事

為什麼約束可以讓我們做更多事情?想像一下今天你是一在一間工廠的生產線上,如果你一下看到蛋糕、一下看到玩具、一下看到螺絲釘、一下看到主機板,這樣是不是會覺得:「嗯?我現在要做啥?」。不過如果這個生產線上出現的都是蛋糕,那我們就會知道這些蛋糕接下來要做什麼處理,比如:分切、裝盒之類的。所以在我們在程式裡做事情的時候我們自然希望取得的東西是我們可以控制的,而不是出來都是Any。

定義泛型

首先我們先建立一些寵物的類別

abstract class Pet(var name: String)
class Cat(name: String) : Pet(name)
class Dog(name: String) : Pet(name)
class Fish(name: String) : Pet(name)

接下來我們來建立為寵物舉辦的競賽,不過因為競賽我們希望可以只限定讓特定的寵物參加, 所以我們使用泛型來約束:

class Contest<T> {
    // ......
}

fun main() {
    val contest = Contest<Dog>() // 舉辦比賽
}

現在我們讓比賽只能限定一種東西可以參加了,可是現在出現了一個問題,就是雖然我們可以限定只有狗狗或是只有貓咪參加比賽,可是我們沒辦法阻止有人舉辦Car的比賽,我們希望可以只限定屬於Pet的類別才可以參加,這時候我們可以這樣做:

class Contest<T : Pet> {
    // ......
}

這樣我們成功限制只能舉辦寵物類別的比賽了

加入屬性與函數

接下來我們必須要建立一個計分板,這樣才能知道參與比賽的寵物分別拿到了多少分數,這樣的話我們會需要一個Map,他的Key是參賽寵物的型態,分數是Int:

class Contest<T : Pet> {
    val scores: MutableMap<T, Int> = mutableMapOf()
    // ......
}

接下來我們來新增參賽寵物的分數,還有看看誰是贏家:

class Contest<T : Pet> {
    private val scores: MutableMap<T, Int> = mutableMapOf()

    fun addScore(t: T, score: Int) {
        if (score < 0) return;
        scores[t] = score
    }

    fun getWinners(): Set<T> {
        val highScore = scores.values.max()
        val winners = mutableSetOf<T>()
        for ((t, score) in scores) {
            if (score == highScore) {
                winners.add(t)
            }
        }
        return winners
    }
}

然後我們來舉辦一場比賽吧!

val contest = Contest<Dog>()
contest.addScore(Dog("小黑"), 85)
contest.addScore(Dog("小固"), 80)
contest.addScore(Dog("白助"), 85)
contest.addScore(Dog("黑皮"), 83)
contest.addScore(Dog("威威"), 81)
val winners: Set<Dog> = contest.getWinners()
for (winner in winners) {
    println(winner.name)
}

我們也可以舉辦一場混合寵物大戰

val contest = Contest<Pet>()
contest.addScore(Dog("小黑"), 84)
contest.addScore(Dog("小固"), 85)
contest.addScore(Fish("波比"), 82)
contest.addScore(Dog("黑皮"), 83)
contest.addScore(Cat("虎小狼"), 85)
val winners: Set<Pet> = contest.getWinners()
for (winner in winners) {
    println(winner.name)
}

開一間寵物店

接下來我們來定義一個寵物零售商的介面,他可以銷售寵物給我們:

interface Retailer<T> {
    fun sell(): T
}

接下來我們實作Retailer介面,讓我們有販賣寵物的零售商們:

class DogRetailer : Retailer<Dog> {
    override fun sell(): Dog {
        return Dog("")
    }
}

class CatRetailer : Retailer<Cat> {
    override fun sell(): Cat {
        return Cat("")
    }
}

我們現在有了寵物的零售商,可以開一間寵物店了,寵物店裡面有販賣各種不同寵物的零售商:

class PetShop {
    val petRetailers: List<Retailer<Pet>> = mutableListOf()
}

可是就在我們要加入各種不同的零售商進去的時候,我們發現我們沒辦法加入定義好的零售商:

class PetShop {
    val petRetailers: List<Retailer<Pet>> = mutableListOf(
        DogRetailer(), 
        CatRetailer(),
    )
    // 這樣會壞掉
}

雖然Dog跟Cat都是Pet的子物件,可是當在泛型裡面的時候編譯器卻沒辦法接受,只能接受Retailer<Pet>的型態,不過好消息是我們可以在Retailer介面裡面做一些調整,讓我們可以順利加入不同的Retailer:

// 加入out
interface Retailer<out T : Pet> {
    fun sell(): T
}

協變(out)

當我們在泛型的前面加上out關鍵字,代表這個泛型型態是“協變”的。協變的意思是,我們可以使用“子型態”來取代“父型態”。所以在加上out關鍵字後,我們可以使用Retailer<Cat>、Retailer<Dog>放到Retailer<Pet>的型態裡面。

在比賽裡面加入獸醫

回到我們前面的比賽裡面,我們發現在比賽中難免會有動物受傷,所以我們需要獸醫在比賽裡面待命,所以我們先建立一個獸醫的類別,並且讓我們可以在比賽裡面指派一位獸醫進駐:

class Vet<T : Pet> {
    fun treat(t: T) {
        println("獸醫治療了${t.name}。")
    }
}

class Contest<T : Pet>(var vet: Vet<T>) {
    // ......
}

接下來我們來試試看獸醫的類別:

val dogVet = Vet<Dog>()
dogVet.treat(Dog("小黑"))

val petVet = Vet<Pet>()
petVet.treat(Cat("虎小狼"))
petVet.treat(Dog("小黑"))

我們發現,Vet<Dog>只能專門治療狗狗,但是Vet<Pet>比較厲害,他所有的寵物都能治療。所以我們在狗狗的比賽裡面請來了一位比較厲害的獸醫師,可是我們驚訝的發現這位獸醫師竟然不得其門而入:

val dogContest = Contest<Dog>(Vet<Pet>())
// 這樣會錯

這樣子很明顯的不合理,不過好加在我們一樣可以透過修改類別來讓這位獸醫師能順利進到比賽裡面:

// 加入in
class Vet<in T : Pet> {
    fun treat(t: T) {
        println("獸醫治療了${t.name}。")
    }
}

反變(in)

當我們在泛型裡面加入in,代表這個泛型是“反變”的。與協變相反,反變可以使用“父型態”取代“子型態”,這樣我們就能讓狗狗比賽可以聘請寵物獸醫。

協變與反變可以只宣告給特定屬性與函數

當我們在類別或是介面裡面宣告in、out的時候,代表這個泛型型態可以在任何地方反變或協變。但是如果我們只想要在特定的地方才能反變或協變的話,我們可以只在屬性或是函數宣告的地方加上in、out這樣就可以區域性的協變、反變。

多泛型參數

當然我們可以定義不只一個泛型,我們可以在宣告類別的時候同時使用兩個泛型(比如:Map),也可以為個別函數添加一個泛型,比如我們想取得一些寵物資訊:

class Contest<T : Pet>(var vet: Vet<in T>) {
    // ......
    fun <R> getPetInfo(infoTranslator: (T) -> R): List<R> {
        val infoList = mutableListOf<R>()
        for (pet in scores.keys) {
            infoList.add(infoTranslator(pet))
        }
        return infoList
    }
}
val dogContest = Contest<Dog>(Vet<Pet>())
dogContest.addScore(Dog("小黑"), 85)
dogContest.addScore(Dog("小固"), 80)
dogContest.addScore(Dog("白助"), 85)
dogContest.addScore(Dog("黑皮"), 83)
dogContest.addScore(Dog("威威"), 81)
val names = dogContest.getPetInfo { it.name }
for (name in names) {
    println(name)
}

reified關鍵字

我們今天出現了一個緊急事件,我們開的寵物店來了一個批發商。他一次提供了好多寵物,全部都混在一起了,我們需要把混在一起的寵物們整理好,這時候該怎麼做呢?

fun <T : Pet> filterPet(pets: List<Pet>): List<T> {
    val list = mutableListOf<T>()
    for (pet in pets) {
        if (pet is T) { // 這裡會錯
            list.add(pet)
        }
    }
    return list
}

我們可能會寫出像是這樣的函數,可是我們會發現這樣的函數會有問題。這是因為泛型在編譯的時候是沒辦法知道他確切是什麼型態的,所以這個泛型會被類型抹去(type erasure),所以我們沒辦法去比對一個被抹除的類型,不過reified關鍵字讓我們可以解決這樣的困境。reified關鍵字必須搭配inline關鍵字使用,他會在編譯的時候保留呼叫函數時的泛型型態,讓我們可以順利的使用泛型來做型態比對

inline fun <reified T : Pet> filterPet(pets: List<Pet>): List<T> {
    val list = mutableListOf<T>()
    for (pet in pets) {
        if (pet is T) {
            list.add(pet)
        }
    }
    return list
}

fun main() {
    val pets = mutableListOf(
        Dog("小黑"),
        Fish("波比"),
        Dog("小固"),
        Cat("虎小狼"),
        Cat("貓卯"),
        Dog("白助"),
        Dog("黑皮"),
    )
    for (dog in filterPet<Dog>(pets)) {
        println(dog.name)
    }
}

Last updated