1. 前言
抽象類別 就是你不能直接用 new 來 new 出來的類別。它的目的是讓你拿來當基底,讓其他更具體的類別去繼承。你可以把它想像成一個「模板」或「藍圖」,它定義了所有子類別都該有的結構和行為,但細節就交給子類別自己決定。
舉個例子比較好懂:想像你有個「交通工具」的概念。現實世界沒有人真的在「交通工具」上移動(除非你在腦內小劇場)。有人開車、有人坐船、有人搭飛機。抽象類別就是你的「交通工具」藍圖,它規定每個交通工具都要有移動的能力(Move),但怎麼移動就要子類別自己決定啦。
// 抽象類別範例
public abstract class Transport
{
public string Model { get; set; }
// 抽象方法 — 沒有實作
public abstract void Move();
}
你如果想 new 一個 Transport 物件,編譯器會直接報錯!你可以試試看,編譯器會直接兇你:「不能建立抽象類別的實例」。
什麼時候要用抽象類別?
- 想把不同但類似的物件的共通特徵和行為集中在一個地方。
- 想要定義一個「統一規範」:比如「所有子類都要會移動」。
- 有些方法可以寫共用邏輯,但有些細節要讓子類自己決定。
抽象類別超適合拿來設計大型系統的架構,當你有一堆共通「功能」但細節要讓每個子類自己搞定時,抽象類別就是你的好朋友。
2. 抽象類別 vs 一般類別
這裡常常會有疑問,所以我們來幫你整理一下!
抽象類別 vs 一般類別
- 一般類別 可以直接用 new 建立,也可以有全部已經寫好的方法。
- 抽象類別:
- 不能直接 new(類別宣告要加 abstract)
- 可以有已經寫好的方法,也可以有抽象(沒內容)的 method
- 可以有欄位、屬性、建構子,甚至 static 方法
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine("動物在睡覺!");
}
public abstract void MakeSound();
}
3. 抽象方法與屬性
抽象方法
抽象方法就是在抽象類別裡宣告,但沒有內容(沒實作)。子類一定要自己實作這個方法。
public abstract class Animal
{
public string Name { get; set; }
// 抽象方法
public abstract void MakeSound();
}
抽象屬性
你也可以宣告抽象屬性,這樣所有子類都必須自己實作它:
public abstract class Shape
{
// 抽象屬性
public abstract double Area { get; }
}
4. 繼承抽象類別
抽象類別可以有抽象方法,也可以有已經寫好的方法。子類一定要實作所有抽象方法和屬性,不然它自己也會變成抽象類別。
我們來延伸一下之前「動物 app」的例子:
public abstract class Animal
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine($"{Name} 睡得很香!");
}
public abstract void MakeSound();
}
現在來寫一個一般(非抽象)類別:
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine($"{Name} 說:汪!");
}
}
你如果不實作 MakeSound(),編譯器就會報錯。
圖解:有抽象類別的類別繼承結構
+---------------------+
| Animal (abstract) |
+---------------------+
| Name: string |
| + Sleep() |
| + MakeSound()* |
+---------------------+
^
|
+------+-------+
| |
Dog Cat
* MakeSound() — 抽象方法
5. 多型實戰:抽象化怎麼用在實務上
我們來玩玩看這些東西在小 app 裡怎麼用。假設你有一個動物清單,裡面有狗跟貓。
List<Animal> animals = new List<Animal>
{
new Dog { Name = "Sharik" },
new Cat { Name = "Barsik" }
};
foreach (var animal in animals)
{
animal.MakeSound(); // 多型呼叫:會跑自己那個
animal.Sleep(); // 呼叫基底類別已經寫好的方法
}
Cat 類別也差不多這樣寫:
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine($"{Name} 說:喵!");
}
}
實務上的好處
抽象類別在大型 app 架構裡超常見。比如說,繪圖軟體通常會有個抽象類別 Shape,裡面有 Draw() 方法和 Area 屬性。各種圖形(像是矩形、圓形)就自己實作這些抽象方法。這樣你就可以一直加新圖形,不用重寫整個程式邏輯。
6. 抽象類別的特點與限制
不能 new 實例
編譯器絕對不讓你這樣搞:
Animal a = new Animal(); // 錯誤!
只有子類(具體類別)才能 new。
抽象類別可以包含:
- 抽象方法宣告(沒實作)
- 已實作的方法(有內容)
- 屬性和欄位(抽象或已實作都行)
- 建構子 — 沒錯,抽象類別可以有建構子,但只能被子類呼叫,不能直接 new。
抽象類別支援繼承
你可以設計一整串抽象類別的繼承結構:
public abstract class Creature
{
public abstract void Live();
}
public abstract class Animal : Creature
{
public abstract override void Live();
public abstract void MakeSound();
}
如果繼承鏈上有類別沒實作所有抽象方法,它自己也要標成抽象類別!
7. 抽象類別與建構子
抽象類別可以有建構子,這些建構子會在子類的建構子裡被呼叫。
重點: 抽象類別的建構子通常會宣告成 protected,這樣就很明顯是給子類用的,不能直接 new。
public abstract class Animal
{
public string Name { get; set; }
// 建構子宣告成 protected
protected Animal(string name)
{
Name = name;
Console.WriteLine($"建立了一隻叫 {name} 的動物");
}
public abstract void MakeSound();
}
public class Dog : Animal
{
public Dog(string name) : base(name)
{
// base(name) 會呼叫 Animal 的建構子
Console.WriteLine("建立了一隻狗");
}
public override void MakeSound()
{
Console.WriteLine($"{Name} 說:汪!");
}
}
為什麼要用 protected 建構子?
- 意圖明確: protected 很明顯就是給子類用
- 防止誤用: 雖然編譯器本來就不讓你 new,但 protected 建構子讓 API 更清楚,完全不會誤會
- 好習慣: 遵守封裝和 API 清晰的原則
你也可以在抽象類別裡用 public 建構子,但 protected 更貼近它的用途。
8. 抽象方法 vs virtual 方法
抽象方法其實就是 virtual 方法的一種(雖然不用寫 virtual),但不能有內容。如果你想讓子類可以改寫(但不是一定要),就用 virtual。如果一定要,就只能用 abstract。
public abstract class Worker
{
// 每個人都要實作
public abstract void Work();
// 可以改寫,也可以用預設
public virtual void Rest()
{
Console.WriteLine("標準休息!");
}
}
| 抽象方法 | virtual 方法 | |
|---|---|---|
| 基底類別有沒有內容 | 沒有,只有宣告(沒內容) | 有,預設內容 |
| 可以在哪宣告 | 只能在抽象類別裡 | 任何沒被 sealed 的類別都可以,包括抽象類別 |
| 子類一定要改寫嗎 | 第一個非抽象子類一定要 | 不一定。可以改寫,也可以用預設 |
| 能不能直接呼叫 | 不能(沒內容) | 可以(會跑預設內容) |
| 主要用途 | 強制子類自己寫內容,定義「規範」 | 讓子類可以改行為(如果想改) |
9. 抽象類別在你的專案裡怎麼用
我們繼續進化我們的學習 app。這次來做一個抽象類別,專門處理使用者輸入(比如不同型態的表單)。
public abstract class InputForm
{
public string Title { get; set; }
// 抽象方法,負責處理輸入流程
public abstract void ShowAndHandleInput();
}
public class LoginForm : InputForm
{
public override void ShowAndHandleInput()
{
Console.WriteLine($"=== {Title} ===");
Console.Write("請輸入帳號:");
string login = Console.ReadLine();
Console.Write("請輸入密碼:");
string password = Console.ReadLine();
Console.WriteLine("驗證完成!");
}
}
public class RegistrationForm : InputForm
{
public override void ShowAndHandleInput()
{
Console.WriteLine($"=== {Title} ===");
Console.Write("請設定帳號:");
string login = Console.ReadLine();
Console.Write("請設定密碼:");
string password = Console.ReadLine();
Console.WriteLine("註冊完成!");
}
}
怎麼用:
InputForm[] forms = new InputForm[]
{
new LoginForm { Title = "登入" },
new RegistrationForm { Title = "註冊" }
};
foreach (var form in forms)
{
form.ShowAndHandleInput();
}
這種寫法在你的程式越來越大時會越有感。抽象類別是打造可擴充系統的好基礎,DRY 跟 SOLID(不要重複自己、寫模組化、好擴充)就是你的好夥伴。
10. 用抽象類別和方法常見的錯誤
錯誤一:忘記實作抽象方法。
如果子類沒實作所有抽象方法和屬性,編譯器就會報錯。這很常發生,尤其是接手別人寫的 code 或是類別很多的時候。記得檢查有沒有漏掉什麼重要的。
錯誤二:想把方法同時標成 abstract 和 static。
這樣不行。抽象方法是給實例用的,static 根本不需要實例。這兩個概念互相衝突,編譯器直接不讓你過。
錯誤三:「偷懶」不想實作所有抽象成員。
你如果不想全部都實作,就只能把類別也標成 abstract。這不算錯,但可能代表你的架構有點亂。有時候可以把部分實作拉到中間的抽象類別,讓子類比較輕鬆。
GO TO FULL VERSION