備註:這邊的抽象是指程式語言中的抽象類別,而非抽象化

抽象的概念很直接,請回想一下前面的例子就可以了:

當我們在用卡牌的例子時,雖然怪獸卡跟魔法卡都繼承了 Card 這個類別,但是我們仍然能
new Card() 來建立一張新卡牌,那…怪怪的吧,這張卡牌到底是什麼呀,空白的卡片嗎?

又或是動物的例子,我們的狗跟貓都繼承了哺乳類,那我們能實例化一個哺乳類嗎?我們的狗跟鳥都是動物,那我們能實例化一個動物嗎?

小明跟小華都繼承了工程師,那我們能 new 一個工程師嗎…?

有些類別就是這樣,它們負責定義共通的那些特性,然而它們本身不應該被實體化成一個物件,這種類別我們就應該把它們標記為抽象類別

抽象類別在 C# 裡用 abstract 這個修飾詞來表示,可以加在類別或方法上。例如 abstract class Animal 就代表動物這個類別是個抽象類別,它不能被實例化。

而當加在方法上時,例如 public abstract void Eat() 就是代表這個進食的方法無法被叫用,只能由繼承者去重新定義這個方法。

abstract class Animal
{
    public string color { get; set; }
    public abstract void Eat();
}

那麼繼承者們,也就是衍生類別如何去重新定義父類別的方法呢?

所謂「欲戴王冠,必 Override」,這時候就必須使用覆寫(override

覆寫是指對於像是前述的抽象方法時,在同名的方法前加上 override 關鍵字就可以讓程式知道你要覆寫這個方法(你不覆寫的話,編譯器還會生氣)。

例如前述的 Eat,狗就可以用 public override void Eat() 的方式去覆寫吃東西這個方法:

public class Dog : Animal
{
    public string color { get; set; } = "Black";
    public override void Eat()
    {
        /* 嚼嚼嚼 */
    }
}

但有時候我們只是希望秉持著多型的精神,讓子類別有可以重新定義的彈性,這時候我們就會使用
虛擬(virtual 的方式去標記這個方法,如此一來就可以實作,同時也讓子類別可以覆寫。

例如可能狗有 public virtual void Eat() 這個進食的方法:

public class Dog
{
    public virtual void Eat()
    {
        /* 嚼嚼熱狗 */
    }
}

那假設我們有個 Giwawa 繼承了 Dog,但牠也是吃熱狗的,就可以選擇不去覆寫 Eat()

public class Giwawa : Dog
{
    /* 不打算實作 Eat,直接使用 Dog 類別的 Eat */
}

而當我們有了 RobotDog 這個類別,它就可以繼承並且重新改寫掉 Eat() 這個方法,從吃肉變成喝汽油。

public class RobotDog : Dog
{
    public override void Eat()
    {
        /* 嚼嚼汽油 */
    }
}

除了使用 override 去覆寫父類別的方法以外,也可以用 new 去隱藏父類別的方法:

public class CyberDog : Dog
{
    public new void Eat()
    {
        /* 嚼嚼汽油 */
    }
}

overridenew 的差別在於多型時轉型成父類別時的行為:

  • override 會直接取代掉父類別的方法,即使轉型為父類別還是以子類別的實作為主
  • new 則是會建立一個子類別專屬的方法,若轉型為父類別就會變回父類別的方法

我們直接用例子來看看吧,假設我們現在有「拉不拉多」和「機器狗」,都繼承了「狗」。差別在於拉不拉多 override 了 Eat() 這個方法,而機器狗 new 了 Eat() 這個方法:

public class Dog
{
    public virtual void Eat() 
    => Console.WriteLine("吃了熱狗");
}

public class Labrador : Dog
{
    public override void Eat() 
    => Console.WriteLine("吃了超大熱狗");
}

public class RobotDog : Dog
{
    public new void Eat() 
    => Console.WriteLine("喝了超多汽油");
}

接著讓我們來看看當他們被實例化之後,以及被轉型為父類別的時候的 Eat() 有什麼不一樣吧:

// Labrador => override
var lala = new Labrador();

lala.Eat(); // 吃了超大熱狗
((Dog)lala).Eat(); // 吃了超大熱狗

// ==============================

// RobotDog => new 
var robot = new RobotDog();
robot.Eat(); // 喝了超多汽油
((Dog)robot).Eat(); // 吃了熱狗

可以看到使用 new 來覆寫的 RobotDog 在被轉型為 Dog 的時候突然就變回吃熱狗了!

要特別注意的是:當你覆寫了父類別的方法,卻忘記加上 override 的話,默認會當成是要 new,所以覆寫的時候還是小心一點,具體地把 overridenew 寫出來吧!

關於抽象和覆寫這部份的範例,因為我個人碰觸的比較少,唯恐我的舉例不夠深入,這邊再附上幾個不錯的範例,可以作為參考:

接著下一篇,我們就接著看這一部分的最後一片拼圖:介面吧!

本系列下一篇:菜雞與物件導向 (7): 介面

參考資料

同系列文章