1. 前言
這堂課我們會聊聊 C# 裡的 繼承語法:學會怎麼宣告衍生類別,搞懂 base 這個關鍵字是什麼,還有怎麼用它來存取父類別。這不只會讓你寫 code 更輕鬆,還能讓你的程式變得更優雅(有時候甚至可以多睡一點,因為不用一直修 copy paste 的 bug)。
來個生活中的簡單例子:我們有個 Vehicle(交通工具)類別——這是個基底、通用的藍圖。所有交通工具都能開、有顏色、有品牌等等:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public void Drive()
{
Console.WriteLine("開車囉!");
}
}
現在我們想加一種新交通工具——汽車。汽車還是交通工具,但有自己的特色(像是門的數量)。如果再寫一次顏色跟品牌就太奇怪了,這些早就定義好了!
繼承 讓我們可以說:「汽車就是交通工具,只是多了很多自己的功能。」
2. 怎麼宣告子類別
在 C# 裡要創建繼承的類別,用冒號 :。在類別名稱後面寫你要繼承的類別。
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
}
可以這樣理解: Car 類別繼承了 Vehicle 的所有欄位和方法,然後再加上自己的東西——像是門的數量。
注意: C# 只支援 單一類別繼承(也就是一個類別只能有一個父類別)。但介面(interface)你想繼承幾個都行!(這個之後會講)
這到底怎麼運作?
當你這樣宣告:
Car myCar = new Car();
...你的 myCar 物件就能用 Vehicle 的所有欄位/方法,也能用自己的屬性。舉例:
myCar.Brand = "Toyota";
myCar.Color = "藍色";
myCar.NumberOfDoors = 4;
myCar.Drive(); // 繼承來的方法
實際情境:
在我們這個「進化中的」應用程式裡,可以做個交通工具選單,讓使用者選類型,然後程式顯示所有屬性。加新交通工具時,你只要寫一點點新 code!
3. 繼承建構子
當我們建立衍生類別的物件時,有時候會想要直接在建構子的時候初始化基底類別的屬性。像是一定要指定品牌跟顏色:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public Vehicle(string brand, string color)
{
Brand = brand;
Color = color;
}
}
現在在子類別的建構子裡就可以(也應該)呼叫基底類別的建構子!這時就要用 base 關鍵字:
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color) // 呼叫 Vehicle 的建構子!
{
NumberOfDoors = numberOfDoors;
}
}
實際上長怎樣?
Car bmw = new Car("BMW", "黑色", 4);
Console.WriteLine($"{bmw.Brand}, {bmw.Color}, 門數: {bmw.NumberOfDoors}");
bmw.Drive(); // 還是可以用!
所以 bmw 既是汽車也是交通工具。它記得自己的「父母」。
4. base 跟 this 的差別
C# 裡有兩個關鍵字可以用來操作類別成員:this 跟 base。
- this — 指的是目前這個物件(自己的欄位或方法)。
- base — 指的是基底類別的成員(也就是父類別)。
什麼時候要用 base?想像你想要擴充基底類別的方法邏輯,或是要從子類別初始化父類別。像這樣:
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }
public Bicycle(string brand, string color, int gears)
: base(brand, color) // 呼叫基底類別的建構子
{
NumberOfGears = gears;
}
public void ShowInfo()
{
// 用基底類別的屬性!
Console.WriteLine($"{Brand} ({Color}), 變速數: {NumberOfGears}");
}
}
5. base 在方法裡的用法
base 不只可以用在建構子!有時候你想要改寫基底類別的方法,但又想用到它的一部分邏輯。
假設我們想讓 Car 類別的 Drive 方法多印出是哪台車:
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color)
{
NumberOfDoors = numberOfDoors;
}
public void DriveCar()
{
base.Drive(); // 呼叫基底實作
Console.WriteLine($"開著 {Brand},有 {NumberOfDoors} 個門!");
}
}
說明:
呼叫 base.Drive(); 就像在說:「爸,你先講你的故事,我再補充細節!」所以呼叫 DriveCar() 會印出:
開車囉!
開著 BMW,有 4 個門!
關於方法覆寫(override)我們下次再聊 :P
6. 繼承結構圖
來把類別關係圖像化一下。這裡有個簡單的圖(UML 早上看真的很可怕!):
Vehicle(基底)
/ \
Car Bicycle
- 所有箭頭都是從子類別指向基底類別。
- 所有子類別都能存取 public/protected 的基底類別成員。
public、protected 跟 private 的差別
小提醒,別搞混了。
在子類別裡可以看到:
| 修飾詞 | 在繼承類別裡可見嗎 |
|---|---|
| public | 可以 |
| protected | 可以 |
| private | 不行 |
常見錯誤:
想在子類別裡存取 private 欄位:
public class Vehicle
{
private string secret = "沒人知道!";
}
public class Car : Vehicle
{
public void RevealSecret()
{
Console.WriteLine(secret); // 錯誤!看不到。
}
}
比較:基底 vs. 衍生類別
| Vehicle | Car(繼承) | |
|---|---|---|
| Brand | 有 | 繼承 |
| Color | 有 | 繼承 |
| Drive() | 有 | 繼承(或擴充) |
| NumberOfDoors | 沒有 | 自己的(獨特屬性) |
7. 範例
如果我們繼續開發這個「交通工具管理」小程式,可以這樣設計:
public class Vehicle
{
public string Brand { get; set; }
public string Color { get; set; }
public Vehicle(string brand, string color)
{
Brand = brand;
Color = color;
}
public void Drive()
{
Console.WriteLine($"{Brand} {Color} 開車囉!");
}
}
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string brand, string color, int numberOfDoors)
: base(brand, color)
{
NumberOfDoors = numberOfDoors;
}
public void ShowCar()
{
Console.WriteLine($"品牌: {Brand}, 顏色: {Color}, 門數: {NumberOfDoors}");
}
}
public class Bicycle : Vehicle
{
public int NumberOfGears { get; set; }
public Bicycle(string brand, string color, int numberOfGears)
: base(brand, color)
{
NumberOfGears = numberOfGears;
}
}
在主方法裡可以建立交通工具清單,然後用不同方式描述它們:
Car ford = new Car("Ford", "紅色", 4);
ford.ShowCar();
ford.Drive();
Bicycle trek = new Bicycle("Trek", "綠色", 21);
trek.Drive();
8. 錯誤、注意事項,還有一點幽默
超常見錯誤——忘記在子類別呼叫基底建構子。如果 Vehicle 沒有無參數建構子,而你又沒呼叫 base(...),就會編譯錯誤:編譯器會抱怨沒辦法初始化基底。這就像想蓋二樓但沒一樓,蓋不起來啦。資料要往上傳才行。
第二個錯誤: 忘記基底類別用 private 宣告的欄位/方法,子類別是看不到的。如果想讓「小孩」也能用,請用 protected(這個之後會細講)。
最重要的: 繼承很方便,但不要搞太複雜的繼承樹。在現實世界裡,如果你的繼承鏈超過兩三層,大概就有點怪怪的。很容易搞混誰繼承誰,然後就會懷疑人生。
GO TO FULL VERSION