第 7 課 · 約 14 分鐘 · ← 第 6 課
繼承與介面
型別之間有兩種關係:繼承("狗是一種動物——繼承它的一切")和介面("機器人會 Speak()——承諾一種能力")。兩者存在的目的都是讓一份程式碼通過同一個形狀操作許多型別。
virtual / override:基類允許你替換行為
class Animal
{
public string Name;
public Animal(string name)
{
Name = name;
}
public virtual void Speak() // virtual:子類可以替換
{
Console.WriteLine(Name + " makes a sound");
}
}
class Dog : Animal // Dog 繼承 Animal
{
public Dog(string name) : base(name) { } // base(...):先跑 Animal 的建構子
public override void Speak() // 替換它
{
Console.WriteLine(Name + " says woof");
}
}
Animal a = new Dog("Rex"); // 狗是一種動物——這個賦值合法
a.Speak(); // 分發跟著物件走:Dog 的版本執行
Animal b = new Animal("Generic");
b.Speak();
最後兩行是全部要點:變數型別是 Animal,但執行的方法是實際物件上的那個——虛分發。一個遍歷混合陣列的迴圈,挨個調 Speak(),每次都叫得恰到好處。
base:在父類的基礎上蓋樓
base(...) 調父類建構子;base.Method() 調父類版本——標準的"擴充套件而非替換"形狀:
class Animal
{
public virtual void Speak() { Console.WriteLine("..."); }
}
class Cat : Animal
{
public override void Speak()
{
base.Speak(); // 先做動物都會做的……
Console.WriteLine("meow"); // ……再加貓的
}
}
is:"它真的是那個嗎?"——順便繫結變數
class Animal { }
class Dog : Animal
{
public void Fetch()
{
Console.WriteLine("fetching!");
}
}
Animal x = new Dog(); // 向上轉:沒問題,Dog 是 Animal
if (x is Dog d) // 測試 + 宣告一步到位
{
d.Fetch(); // 只有狗會撿東西
}
安靜的兄弟 as 負責轉換、失敗時給 null:Dog dd = x as Dog;——然後檢查 dd != null。只用來分支用 is;轉換後還要用值用 as。
介面:是承諾,不是血緣
interface IGreeter
{
void Greet(); // 一紙契約:沒有方法體,只有承諾
}
class EnglishGreeter : IGreeter
{
public void Greet() { Console.WriteLine("Hello"); }
}
class SpanishGreeter : IGreeter
{
public void Greet() { Console.WriteLine("Hola"); }
}
IGreeter[] greeters = { new EnglishGreeter(), new SpanishGreeter() }; // 互不相干的類,同一個承諾
foreach (IGreeter g in greeters)
{
g.Greet(); // 各自履行各自的行為
}
兩個問候者沒有共同基類——介面不是 ancestry,是共同的承諾。這就是為什麼 C# 允許一個類實現很多介面、卻只能繼承一個基類:血緣排他,承諾不排他。
哪個工具,什麼時候?
| 場景 | 用 |
|---|---|
| "X 是一種 Y",共享程式碼 | 繼承(class Dog : Animal) |
| "X 能做 Z",橫跨不同家族 | 介面(: IComparable) |
| 子類微調父類行為 | virtual + override |
| 子類擴充套件父類行為 | override + base.Method() |
| 安全向下轉後再用額外成員 | is(分支)/ as(保留) |
現在就練
- 建
Shape帶virtual double Area() { return 0; },再建Square和Circle重寫它——遍歷Shape[]列印每個面積。(練習場的示例裡有一份起步程式碼。) - 只給其中一個形狀加
IScalable(含void Scale(double factor))——證明介面可以橫切家族樹。 - 下一課:第 8 課 — 集合。