程式遺傳學

為什麼我們應該使用繼承?

我們在設計程式的時候常常會遇到有“共同特性”的類別,比如:小轎車、大卡車、電動車,他們都是“車”、狗、貓、鳥、魚,他們都是“動物”。這些類別有著某些相同的屬性與行為,我們自然會想辦法不要一直重複撰寫同樣的東西,而“繼承”就能很好的解決上述的困擾。

什麼是繼承

當我們在寫程式碼的時候,如果有很多類別擁有相同的程式碼,我們就會將共同的程式碼放在一個類別裡面,這樣繼承這個類別的其他類別就可以使用相同的這一段程式碼。如果我們需要修改的話,只需要修改一個地方,我們所做的修改會反應在所有繼承的類別上面。

而這個擁有共同程式碼的類別我們稱之為父類別或是超類別(superclass),繼承他的類別我們稱為子類別(subclass)

測試繼承關係

可是並不是所有擁有相同程式碼的東西都適合使用繼承,比方說:動物會叫,所以狗狗會叫,狗狗繼承動物得到“叫”的特性很合理。可是另一個情況:冷氣可以降低室溫,教室裡面有冷氣可以降低室溫,但是教室繼承冷氣得到“降低室溫”這個特性感覺好像哪裡怪怪的,應該是教室裡面有冷氣,冷氣可以降低室溫這樣才是對的,教室本身並沒有降低室溫這樣的功能。

所以我們在繼承類別的時候我們應該要問自己一些小問題“XXX IS-A YYY?”還是“XXX HAS-A YYY?”

上述兩個例子我們就可以帶入看看“狗狗 IS-A 動物”還是“狗狗 HAS-A 動物”呢?另外一個例子“教室 IS-A 冷氣機”還是“教室 HAS-A 冷氣機”呢?

在繼承關係裡面如果X類別繼承了Y類別,那“X IS-A Y”應該要正確,如果這個關係沒辦法通過IS-A測試的話,X就不應該繼承Y。

怎麼繼承類別?

首先我們先定義好Animal類別

class Animal {
    val food = ""

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

    fun makeNoise() {
        println("The animal is making noise.")
    }
}

在Animal這個類別裡面我們宣告了動物這個類別,有喜歡吃的食物,會進食也會叫。那要怎麼繼承呢?

class Dog : Animal() {  }

我們使用“:”來繼承父類別,並且要在繼承的時候呼叫父類別的建構式,這樣就是繼承的寫法。可是現在卻出現了紅字,原來在Kotlin裡面為了防止類別被胡亂繼承,所以一般情況下類別是無法被繼承的,要讓一個類別可以被繼承我們需要在前面加上open關鍵字

open class Animal {
    // ......
}

這樣我們就成功的讓Dog繼承Animal了。

如果想要修改父類別的屬性或函數......

這時Dog繼承了Animal的所有屬性以及函數,可是並不是所有的屬性跟函數都符合Dog的需求,我們必須“覆寫(override)”他,而在我們要覆寫的屬性前面都必須加上open才可以覆寫

覆寫屬性

在覆寫的時候我們需要使用override關鍵字,我們使用override成功覆寫了food的屬性,成功修改了屬性的預設值:

open class Animal {
    open val food = ""
    // ......
}
class Dog : Animal() {
    override val food: String = "狗飼料"
}

屬性的覆寫還可以做得更多

除了覆寫預設值之外我們還可以利用覆寫來做的事情有:

  • 可以覆寫屬性的getter與setter

  • 可以把val屬性覆寫成var

  • 可以把屬性的型態覆寫成該型態的子型態

覆寫函數

覆寫函數跟覆寫屬性一樣父類別的函數必須加上open,並且在子類別使用override關鍵字覆寫他:

open class Animal {
    // ......
    open fun makeNoise() {
        println("The animal is making noise.")
    }
}
class Dog : Animal() {
    // ......
    override fun makeNoise() {
        println("Woo ou ou ou o ou ooof!")
    }
}

如果我們要在繼承的類別使用父類別的函數,可以使用super關鍵字:

class Dog : Animal() {
    // ......
    override fun makeNoise() {
        super.makeNoise()
        println("Woo ou ou ou o ou ooof!")
    }
}

在覆寫函數的時候我們必須遵守這些規則:

  • 子類別函數的參數必須符合父類別函數的參數

  • 函數回傳的類型必須要是父類別函數的型態或是該型態的子型態

覆寫的屬性或函數會保持open

在Dog中被覆寫過的屬性跟函數也會繼續維持open的狀態,所以如果我們把Dog也加上open關鍵字,並且讓TaiwaneseDog類別去繼承他的話,我們也一樣可以在TaiwaneseDog裡面覆寫這些屬性與函數。如果我們不想再讓子類別覆寫某一個屬性或者函數,我們可以在前面加上“final”關鍵字

使用繼承的類別

現在我們成功讓Dog繼承並且覆寫了Animal裡面一些屬性與函數,那我們可以怎麼樣使用他呢?

可以使用父類別的地方就可以使用他的子類別

在繼承的時候子類別會繼承父類別的所有東西,換言之所有父類別可以使用的地方子類別都可以使用:

val dog = Dog()
val animal: Animal = Dog()

我們可以看到“dog”這個變數型態為Dog,但是“animal”變數雖然我們把他的型態宣告為Animal,可是我們依然可以放Dog進去,這個性質也一併適用在函數的參數與回傳型態上

類型檢測

我們可以使用“is”關鍵字來檢查某個變數是否為某一個型態:

val dog = Dog()
val animal: Animal = Animal()
println(dog is Dog)
println(dog is Animal)
println(animal is Dog)
println(animal is Animal)

在這個例子裡面我們可以看到,我們在檢查的時候子類別可以為父類別的類型,但是父類別不可以為子類別的類型。

類型轉換

我們可以使用“as”來為變數做強制轉型

val animal: Animal = Dog()
val dog = animal as Dog

而使用“as”來轉型是危險的,如果轉型不成功就會拋出錯誤。比較安全一點的方式是使用“as?”來轉型,如果轉型失敗的話會得到null

智慧類型轉換

在某一些狀況編譯器可以在類型檢測後確定變數的型態(if/when條件判斷式、while迴圈),這個時候我們不需要用as做強制轉型編譯器就會聰明的幫我們做自動轉型

val animal: Animal = Dog()
    if (animal is Dog) {
        // 這個時候animal可以被視為Dog型態
    }

在子類別裡面使用父類別的屬性或函數

在子類別裡面可以使用“super”關鍵字,我們可以使用super來使用父類別裡面非private修飾的屬性與函數

當我們呼叫變數的屬性與函數的時候,我們呼叫的是實體類型的屬性與函數

val animal1: Animal = Dog()
val animal2: Animal = Animal()
animal1.makeNoise()
animal2.makeNoise()

當我們執行上面的程式碼的時候,我們會發現同樣都是Animal類型的變數,呼叫makeNoise函數之後會跑出不一樣的結果。這是因為當我們在呼叫變數的時候決定要執行哪一個類型的東西的關鍵並不是在變數的類型,而是“變數裡面實體物件的類型”。

所以我們可以在變數、集合等各種不同的地方利用相同的父類別容納各種不同類型的子類別,而且這些子類別都可以順利展現各自的行為,這個也是繼承帶給我們的好處之一,可以讓我們的程式更有彈性。而這種相同的函數可以在不同的子類別裡面實作的機制我們稱為“多型”

Last updated