更多的多型

前面在繼承的時候我們用狗狗跟動物來示範了繼承的關係,可是在這個例子裡面有一些問題存在。

不應該被實體化的類別

狗狗可以被實體化,因為當我們講到狗的時候,我們可以很輕易的在腦海裡想像出一隻狗應該要長成什麼樣子。可是動物呢?動物長什麼樣子?他應該要能被實體化嗎?這種我們很難想像出實體的類別,或是應該說我們不想讓他實體化的類別,我們就可以把他改成抽象類別

將Animal改成抽象

我們使用“abstract”關鍵字來宣告抽象類別,當我們使用abstract之後就不需要再把這個類別宣告成open了, 抽象類別會另外附帶open的性質

abstract class Animal {
    // ......
}

當我們把Animal改成抽象之後,就沒辦法把他實體化了,雖然我們還是可以使用Animal當作變數的型態並且把他的子類別放進去使用,不過我們就不需要擔心有人可以把Animal實體化,因為編譯器會阻止這件事情發生。

var animal: Animal
animal = Dog()
animal = Animal() // 這裡會出錯

不過雖然Animal不能再被實體化,我們仍然可以為他定義建構式,並且我們在繼承的時候一樣要呼叫父類別的建構式。

抽象類別裡面的抽象屬性與函數

在Animal裡面我們還留著之前在定義類別的時候留下來的預設實作,可是這些實作其實並沒有很必要,所以我們可以把他們也宣告成抽象

abstract class Animal {
    abstract val food: String

    abstract fun makeNoise()

    fun eat() {
        println("The animal is eating $food.")
    }
}

當屬性被標注成抽象後,就不能賦予初始值,也不能實作getter、setter。同樣的函數被標注成抽象後,也要把“{ }”裡面的實作移除。這是因為如果我們把屬性、函數宣告成抽象就代表我們認為他們在此時此刻沒有合適的實作方式,所以編譯器也會阻止我們在這個時候實作他們。

‘抽象的屬性跟函數感覺好奇怪,既然沒辦法實作,為什麼我們還需要去定義他們?’

“雖然我們無法實作抽象屬性與函數,可是抽象提供了一個共同的協議讓子類別們去實作,這樣我們就可以當成多型使用。因為每個子類別都一定得要實作抽象的屬性與函數,所以當我們在使用父類別的型態的時候,我們可以確定子類別都有相同的屬性跟函數可以使用”

使用介面實作共同行為

我們現在已經知道要怎麼繼承抽象介面了,不過我們現在想要在Animal裡面加上一個“move”函數,讓所有的動物都可以走來走去,而這個時候我們想要定義一個“Car”類別。我們會發現Car一樣也可以動過來動過去,所以裡面也會有一個move函數,可是當我們做“Car IS-A Animal”的時候會覺得這個講法很奇怪,明顯不符合常理。這個時候可以怎麼辦呢?

介面可以在父類別“之外”定義共同的行為

抽象可以為相同種類的類別定義一個協定,而介面則是可以為共同的“行為”定義一個協定,跟實作抽象一樣,實作介面也可以讓我們獲得多型帶來的好處。不過跟抽象還有繼承不一樣的是:一個類別可以實作多個介面,但是只能繼承一個抽象或是open類別。所以介面可以讓我們獲得多型的好處的同時,也能保持更多的彈性。

定義介面

依照上面提到的例子我們來定義Movable介面

interface Movable {
    fun move()
}

我們看到在Movable裡面定義了一個move函數,這個move沒有實作,所以他是一個抽象函數。可是他並沒有abstract關鍵字,這是因為在介面裡面編譯器會自動把所有沒有實作的函數當成是抽象的,所以我們不需要特別寫出來

介面裡面的函數可以是抽象也可以是具體的

我們也可以為介面裡面的函數提供預設實作:

interface Movable {
    fun move() {
        println("This things is moving.")
    }
}

介面裡的屬性

我們也可以將屬性加入介面裡面,不過因為介面不能宣告建構式,所以必須宣告在介面裡面

interface Movable {
    val speed: Int
    // ......
}

跟函數一樣的地方是我們不需要特別加上abstract關鍵字,這個屬性會自動被視為抽象屬性,因為是抽象屬性所以我們沒辦法初始化他,不過我們可以幫他定義預設的getter、setter

interface Movable {
    var speed: Int
        get() = 20
    // ......
}

不過我們在實作預設setter的時候需要注意,因為介面的屬性沒有幕後屬性,所以不能使用field關鍵字

interface Movable {
    var speed: Int
        get() = 20
        set(value) {
            println("Cannot use field here.")
        }
    // ......
}

實作介面

跟繼承抽象類別時一樣,當我們要實作介面的時候也是用一樣的方式,只是介面沒有建構式,所以也不需要在實作的時候呼叫建構式

class Car : Movable {
    // ......
}

覆寫介面的屬性與函數

覆寫函數的方式跟覆寫抽象函數的方式一樣,不過如果介面的函數有預設實作的話,我們可以不覆寫他

class Car : Movable {
    override fun move() {
        println("This car is moving. Speed is $speed km/h.")
    }
}

再來我們看看實作屬性的方式,我們可以實作屬性的getter、setter

class Car : Movable {
    override var speed: Int
        get() = TODO()
        set(value) {  }
    // ......
}

或是給予初始值

class Car : Movable {
    override var speed: Int = 20
    // ......
}

最後,我們也可以把他實作在建構式裡

class Car(override var speed: Int) : Movable {
    // ......
}

以上這些方式在實作抽象屬性的時候也一併適用

我們可以實作多個介面!

前面我們講到我們可以實作多個介面,接下來我們來看看要怎麼實作

class X : A, B {
    // 類別X實作A與B介面
}

class Y : C(), A, B {
    // 類別Y繼承C類別並實作A與B介面
}

如同繼承類別的時候,如果介面有預設實作的話,我們可以在實作的類別裡面使用super來呼叫預設實作的函數。如果在繼承或是實作的時候,繼承的類別跟介面有相同名字的函數,我們可以使用“super<A>”去指定要呼叫哪一個版本的預設函數。

Last updated