1. 抽象方法
在上一堂課我們打好了基礎:認識了抽象類別,也看到它們可以包含抽象方法和屬性。我們明白這是一種「契約」,強制繼承的類別一定要自己實作這些東西。
現在我們來深入一點,看看更複雜、更實用的情境。這堂課會聚焦在語法細節、抽象在多層繼承裡怎麼運作,還有什麼時候該用 abstract,什麼時候該用 virtual。
抽象方法就像一本老食譜裡的神祕配方:「加點祕密材料」——到底是什麼沒說,但後面的廚師都得自己想辦法!
抽象方法一定要在抽象類別裡
如果你想在一般(非抽象)類別裡宣告抽象方法,編譯器會直接報錯。為什麼?因為一般類別可以直接 new 出來,如果你呼叫一個沒寫內容的方法,會發生什麼事?只會得到 IDE 一臉問號。
// 這會編譯失敗:
public class WrongClass
{
public abstract void Oops(); // 這樣不行!
}
只要類別裡有一個抽象方法,它就一定要標成 abstract!
在子類別實作抽象方法
實作時要用 override 關鍵字。如果你繼承抽象類別但沒實作所有抽象方法,那這個類別也必須是抽象的(不然編譯器會抗議)。
繼續我們的應用程式:
在前面的課程我們做了一個 Shape(圖形)類別,裡面有個抽象方法算面積。現在來做幾個具體的圖形:
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; }
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
為什麼抽象方法這麼重要?
抽象方法就是你在說:「各位,如果你要繼承這個類別,請自己決定這東西怎麼做!」這讓程式架構更清楚,避免漏掉重要東西,最重要的是可以讓多型(polymorphism)發揮到極致。
2. 抽象屬性(properties)
什麼是抽象屬性
抽象屬性就跟抽象方法一樣是契約,只是針對屬性(property)。對 C# 來說這很自然,因為屬性是封裝資料的主要方式。抽象屬性就是:「讓子類自己決定這個屬性值要怎麼來(甚至怎麼設定)」。
範例:
給圖形加個 Name 屬性——每個子類都要有,但怎麼回傳由自己決定。
public abstract class Shape
{
public abstract string Name { get; }
public abstract double CalculateArea();
}
現在每個子類都必須實作這個屬性:
public class Rectangle : Shape
{
public double Width { get; }
public double Height { get; }
public override string Name => "矩形";
public Rectangle(double width, double height)
{
Width = width;
Height = height;
}
public override double CalculateArea()
{
return Width * Height;
}
}
public class Circle : Shape
{
public double Radius { get; }
public override string Name => "圓形";
public Circle(double radius)
{
Radius = radius;
}
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
語法特色
抽象屬性跟 interface 很像:宣告時沒有內容,但要指定是只有 getter,還是也有 setter。
public abstract class Creature
{
// 唯讀屬性
public abstract string Species { get; }
// 可讀可寫屬性
public abstract int Age { get; set; }
}
在實作類別裡可以自己決定怎麼取得(或設定)這個值。
為什麼要用抽象屬性
抽象屬性超適合你想讓每個子類自己決定怎麼取得或存這個值。這在建模、商業邏輯、view model,或是屬性邏輯比單純回傳欄位還複雜時很常用。
比如有的圖形名字要算出來,有的直接回傳常數,都可以用抽象屬性「藏」起來。
3. 圖解:抽象類別、方法和屬性的關係
來看個圖,感受一下怎麼運作:
┌─────────────┐
│ abstract │
│ Shape │
│-------------│
│ +Name: str │ <-- 抽象屬性
│ +Area(): dbl│ <-- 抽象方法
└─────┬───────┘
│
┌────▼────┐ ┌───────┐
│Rectangle│ │ Circle│
│........ │ .... │ ......│
│+Name │ │+Name │
│+Area() │ │+Area()│
└─────────┘ └───────┘
這樣我們就能用同一個介面操作任何 Shape 子類,但每個子類「底下」怎麼做都沒差。
UML 圖:抽象方法和屬性
┌────────────────────────────┐
│ abstract class │
│ Animal │
│────────────────────────────│
│+ Name: string {abstract} │
│+ MakeSound(): void {abstract}│
└─────────────┬──────────────┘
│
┌────────┴──────────┐
│ │
┌────────────┐ ┌─────────────┐
│ Cat │ │ Dog │
│────────────│ │─────────────│
│+ Name │ │+ Name │
│+ MakeSound()│ │+ MakeSound()│
└────────────┘ └─────────────┘
4. 實務情境與專案好處
抽象方法和屬性就是你設計會進化的類別繼承時的利器。這是大型商業應用、多層領域模型、plugin 系統、UI framework 的基礎,讓系統底層有「規則」,所有子類都得遵守。
在真實專案裡,這種「純」架構設計讓你幾個月、幾年後加新功能時很安心,因為舊程式早就考慮到所有行為。
「為什麼」與多型的回歸
多型看起來像魔法,其實就是契約:「你可以對任何這家族的物件呼叫某個方法,一定會得到正確結果」。抽象方法和屬性就是讓整個繼承鏈都講同一種語言,但不用硬塞預設實作。
這種做法在 plugin 系統(IDE、繪圖軟體、CRM 系統)很常見,外部開發者寫擴充時,必須實作平台要求的最小功能。例如你寫檔案處理模組,基底類別可以有抽象屬性 FileExtension 和抽象方法 Open(),這樣任何 plugin 都能正確處理自己的檔案類型。
抽象、虛擬和一般類別成員的差異
| 特性 | 一般方法/屬性 | virtual | abstract |
|---|---|---|---|
| 有沒有實作 | 有 | 有 | 沒有 |
| override 必須嗎 | 不用 | 不用(可選) | 要(子類必須) |
| 能不能直接呼叫 | 可以 | 可以 | 不行 |
| 能不能在一般類別裡 | 可以 | 可以 | 不行 |
| 能不能標 sealed | 不行 | 可以 | 不行 |
5. 在我們的學習應用裡的應用
現在有了新知識,回到我們的圖形應用,看看抽象屬性和方法怎麼一起打造彈性又好懂的系統。
// 例如在主程式裡
List<Shape> shapes = new List<Shape>
{
new Rectangle(4, 5),
new Circle(2.5)
};
foreach (Shape shape in shapes)
{
// 靠抽象屬性 Name 和方法 CalculateArea,
// 我們不用管圖形的實際型別
Console.WriteLine($"{shape.Name}, 面積: {shape.CalculateArea():F2}");
}
結果:
矩形, 面積: 20.00
圓形, 面積: 19.63
很美吧:程式根本不在意圖形是誰——只要呼叫屬性和方法,.NET CLR 會自動找到正確的實作!
6. 常見錯誤與實作細節
錯誤一:沒實作父類的抽象方法。
如果子類有任何抽象方法或屬性沒實作,自己又沒標 abstract,編譯器就不讓你過。這很有用:你不會 new 出一個「有漏洞」的物件——比如沒有 CalculateArea() 的圖形。
錯誤二:方法標成 abstract,但類別沒標。
這種也不能編譯。如果你加了 abstract 方法,類別本身也要是抽象的:
public abstract class Polygon : Shape
{
// 沒實作 CalculateArea(),類別還是 abstract
}
錯誤三:想用 virtual 來實作抽象方法。
抽象方法只能用 override 來實作,不能用 virtual。但如果你想讓後面的子類還能再改寫,可以先 override,然後在子類裡標成 virtual:
public override double CalculateArea()
{
// 預設實作...
}
更深一層的子類就能再 override 這個方法。但不能再標成 abstract,因為已經有實作了。
GO TO FULL VERSION