CodeGym /課程 /C# SELF /介面與抽象類別的差異

介面與抽象類別的差異

C# SELF
等級 23, 課堂 1
開放

1. 一體兩面

我們已經蠻深入聊過抽象這個概念,也看過 C# 裡兩個超強大的工具:抽象類別 和 介面。你應該已經感覺到它們很像,因為兩者都能定義「行為骨架」,讓具體類別去實作。但說真的,這就像拿錘子跟螺絲起子比:都是工具,但用途完全不同啦。

先來說重點:為什麼要有兩種工具?在寫程式跟人生一樣,沒什麼是「只是剛好」。如果有兩個看起來很像的工具,那一定各有強項和適用場景。

我們先用一張簡單的比較表來看一下,這算是小抄,幫你抓住最重要的差異。

特性 抽象類別 介面
建立實例 不能直接建立 (
new AbstractClass()
).
不能直接建立 (
new IMyInterface()
).
成員實作 可以有:
– 完整實作的方法/屬性。
– 抽象方法/屬性(沒實作)。
– 欄位、建構子、靜態方法。
可以有預設實作的方法、靜態抽象和靜態非抽象成員。
不能有實例欄位和建構子。
存取修飾詞 可以有任何修飾詞(public、protected、internal、private)。 介面成員預設都是 public。不用寫修飾詞(例外:有預設實作的方法和靜態成員)。
繼承 類別只能繼承一個抽象類別。 類別可以實作多個介面。
關係型態 「是」(is-a)。定義基礎型態和共通階層。 「能做」(has-a 或 can-do)。定義行為/能力契約。
狀態 可以存狀態(實例欄位)。 可以有靜態欄位,但不能有實例欄位。
擴充 可以加新實作方法,不會影響繼承類別。 可以加預設實作的方法,不會破壞繼承類別。

是不是蠻酷的?我們來細講這些重點。

2. 多重繼承

這大概是最基本也最好記的差異。在 C#(還有像 Java 這種語言)裡,一個類別只能繼承一個父類。不管是普通類別還是抽象類別,反正就是只能一個!這是為了避免有名的「菱形問題」(Diamond Problem),就是多重繼承時,兩個父類有同名方法,結果不知道該用哪一個。

但介面你想實作幾個都行!想像你的類別像一個人,他可以是「學生」(繼承 Student),但同時「會煮飯」(ICookable)、「會開車」(IDriveable)、「會唱歌」(ISingable)。超彈性的啦!


abstract class Animal
{
    public string Name;
    public abstract void MakeSound();
    public void Eat() => Console.WriteLine($"{Name} 吃東西。");
}

interface IFlyable { void Fly(); double MaxFlyingAltitude { get; } }
interface ISwimable { void Swim(); }

class Duck : Animal, IFlyable, ISwimable
{
    public double MaxFlyingAltitude => 1000;
    public override void MakeSound() => Console.WriteLine($"{Name} 呱呱叫!");
    public void Fly() => Console.WriteLine($"{Name} 飛起來!");
    public void Swim() => Console.WriteLine($"{Name} 游泳!");
}

class Program
{
    static void Main()
    {
        var duck = new Duck { Name = "唐老鴨" };
        duck.MakeSound();
        duck.Eat();
        duck.Fly();
        duck.Swim();

        // 用不同型態操作物件:
        Animal a = duck;  a.Eat();
        IFlyable f = duck; f.Fly();
        ISwimable s = duck; s.Swim();
    }
}

如果你的類別是某個階層的一部分(像 Dog 是 Animal),就用類別繼承(抽象或普通都行)。如果你的類別有某種能力(像 Dog 會 跑,Cat 會 跑),但這些能力跟階層沒關係,就用介面。這就是介面的最大優勢——可以給完全沒關係的類別一樣的「能力契約」。

3. 哪裡有資料,哪裡只有承諾?

抽象類別可以放任何東西:

  • 普通(非抽象)方法和屬性,直接有實作。
  • 抽象方法和屬性(沒實作,要子類去寫)。
  • 欄位(實例變數),可以存物件狀態。
  • 建構子,初始化狀態用。
  • 甚至靜態方法和屬性。
  • 還有各種存取修飾詞:public、protected、private 等等。

這讓抽象類別變成定義部分實作行為和共通狀態的超強工具。


abstract class Employee
{
    public string FirstName, LastName;
    public decimal Salary { get; protected set; }
    public Employee(string first, string last) { FirstName = first; LastName = last; }
    public void GetPaid(decimal sum)
    {
        Salary += sum;
        Console.WriteLine($"{FirstName} {LastName} 拿到 {sum:C}。薪水:{Salary:C}");
    }
    public abstract void PerformWork();
    public abstract void TakeBreak();
}

class Developer : Employee
{
    public Developer(string f, string l) : base(f, l) { }
    public override void PerformWork() => Console.WriteLine($"{FirstName} 寫程式。");
    public override void TakeBreak() => Console.WriteLine($"{FirstName} 喝咖啡。");
}

class Tester : Employee
{
    public Tester(string f, string l) : base(f, l) { }
    public override void PerformWork() => Console.WriteLine($"{FirstName} 找 bug。");
    public override void TakeBreak() => Console.WriteLine($"{FirstName} 踢足球。");
}

class Program
{
    static void Main()
    {
        Employee[] team = {
            new Developer("伊凡", "彼得羅夫"),
            new Tester("瑪麗亞", "西多羅娃")
        };

        foreach (var emp in team)
        {
            Console.WriteLine($"\n--- {emp.FirstName} {emp.LastName} 的工作日 ---");
            emp.PerformWork();
            emp.GetPaid(2000);
            emp.TakeBreak();
        }
    }
}

介面就完全不一樣了。C# 8 以前只能有方法、屬性、索引子、事件的宣告,不能有欄位、建構子、方法實作!就是只有「簽名」沒內容。所有成員預設都是public(就算你沒寫 public 也一樣)。這保證介面就是純契約,沒有任何實作細節或隱藏狀態。

C# 8 以後,介面可以有預設實作的方法(Default Interface Methods)和靜態成員。這是為了讓你可以在不破壞舊程式碼的情況下,給介面加新方法。但就算這樣,介面還是不能有實例欄位和建構子。這個限制很重要,讓介面永遠只是「行為契約」,不是「資料倉庫」。


interface ISaveable
{
    void Save(string file); 
    bool IsDirty { get; }
}

interface ILoadable
{
    void Load(string file);
}

class GameProgress : ISaveable, ILoadable
{
    public int Level { get; set; }
    public string PlayerName { get; set; }
    bool _isDirty = true;
    public bool IsDirty => _isDirty;

    public GameProgress(string name, int level)
    {
        PlayerName = name; Level = level;
    }
    public void Save(string file)
    {
        Console.WriteLine($"儲存:{PlayerName},等級 {Level} -> {file}");
        _isDirty = false;
    }
    public void Load(string file)
    {
        Console.WriteLine($"從 {file} 載入");
        PlayerName = "新玩家"; Level = 5; _isDirty = true;
    }
    public void UpdateProgress(int newLevel)
    {
        Level = newLevel; _isDirty = true;
        Console.WriteLine($"更新到 {Level}。");
    }
}

class Program
{
    static void Main()
    {
        var game = new GameProgress("英雄", 1);
        game.UpdateProgress(3);

        ISaveable saver = game;
        if (saver.IsDirty) saver.Save("save.dat");

        ILoadable loader = game;
        loader.Load("save.dat");
        Console.WriteLine($"載入後:{game.PlayerName}, {game.Level}");
    }
}

結論:如果你要一個基底類別,除了契約還要有共用邏輯或共用狀態,就用抽象類別。如果你只要「行為清單」或「約定」,完全不需要實作或狀態,那就用介面。

4. 什麼時候該用介面?

介面超適合用來定義能力或行為,讓完全沒關係的物件都能有。例如:

  • IDisposable:任何需要「釋放」的物件(檔案、網路連線、資料庫)。
  • IEnumerable<T>:任何可以用 foreach 迴圈跑的物件。
  • IComparable<T>:任何可以跟同型別物件比較的東西。

重點是,FileStream 跟 SqlConnection 都能實作 IDisposable,List<T> 跟 Dictionary<TKey, TValue> 都能實作 IEnumerable<T>。這些類別完全不同,但有共同能力,就是介面定義的。

舉例:想像你有 汽車 和 飛機。兩者都可以是 交通工具(也許是抽象類別)。但 汽車、飛機,甚至 船(如果你加進來)——都能移動。這個「能移動」就是 IMovable 介面的最佳範例。


interface IMovable
{
    void Move(int distance);
}

class Car : IMovable
{
    public string Brand;
    public Car(string brand) => Brand = brand;
    public void Move(int d) => Console.WriteLine($"{Brand} 開 {d} 公里。");
}

class Airplane : IMovable
{
    public string Model;
    public Airplane(string model) => Model = model;
    public void Move(int d) => Console.WriteLine($"{Model} 飛 {d} 公里。");
}

class Human : IMovable
{
    public string Name;
    public Human(string name) => Name = name;
    public void Move(int d) => Console.WriteLine($"{Name} 走 {d} 公尺。");
}

class Program
{
    static void Main()
    {
        IMovable[] movers = { new Car("Toyota"), new Airplane("Boeing 747"), new Human("阿圖爾") };
        foreach (var item in movers)
            item.Move(100);
    }
}

有沒有發現?我們可以做一個 IMovable 清單,然後對每個元素呼叫 Move(),完全不用管它到底是 Car、Airplane 還是 Human。這就是介面多型的威力!

5. 什麼時候該用抽象類別?

抽象類別超適合定義一群緊密相關類別的共用功能,這些類別都是某種東西的變種。它們可以提供大家都需要的共用程式碼,同時強制子類去實作各自的細節。

想像你有一堆不同的銀行帳戶:SavingAccount(儲蓄)、CheckingAccount(活期)、CreditAccount(信用)。它們都是 BankAccount。大家都有餘額(Balance),都能存錢(Deposit)。但提款規則(Withdraw)每種帳戶都不一樣。這時就該用抽象類別 BankAccount!


abstract class BankAccount
{
    public string AccountNumber { get; }
    public decimal Balance { get; protected set; }
    public BankAccount(string acc) { AccountNumber = acc; }
    public void Deposit(decimal sum)
    {
        if (sum > 0)
        {
            Balance += sum;
            Console.WriteLine($"{AccountNumber}: +{sum:C}, 餘額:{Balance:C}");
        }
    }
    public abstract bool Withdraw(decimal sum);
}

class CheckingAccount : BankAccount
{
    public CheckingAccount(string acc) : base(acc) { }
    public override bool Withdraw(decimal sum)
    {
        if (Balance >= sum)
        {
            Balance -= sum;
            Console.WriteLine($"{AccountNumber}: -{sum:C}, 餘額:{Balance:C}");
            return true;
        }
        Console.WriteLine($"{AccountNumber}: 餘額不足");
        return false;
    }
}

class CreditAccount : BankAccount
{
    public decimal CreditLimit { get; }
    public CreditAccount(string acc, decimal limit) : base(acc) => CreditLimit = limit;
    public override bool Withdraw(decimal sum)
    {
        if (Balance - sum >= -CreditLimit)
        {
            Balance -= sum;
            Console.WriteLine($"{AccountNumber}: -{sum:C}, 餘額:{Balance:C}");
            return true;
        }
        Console.WriteLine($"{AccountNumber}: 超過額度");
        return false;
    }
}

class Program
{
    static void Main()
    {
        var checking = new CheckingAccount("12345");
        checking.Deposit(1000);
        checking.Withdraw(300);
        checking.Withdraw(800);

        Console.WriteLine("\n--- 信用帳戶 ---");
        var credit = new CreditAccount("67890", 500);
        credit.Deposit(200);
        credit.Withdraw(400);
        credit.Withdraw(400);
    }
}

這裡 BankAccount 給所有帳戶共用的存錢邏輯(Deposit),管理帳號和餘額。但提款邏輯(Withdraw)每種帳戶都不一樣,所以是抽象的。

6. 兩者一起用:完美搭檔

最優雅、最強大的設計,常常是抽象類別和介面一起用。抽象類別本身也可以實作一個或多個介面!

想像一下:你有 abstract Animal,定義基本東西。但有些 Animal 可能是 IMovable、ICarnivore、IPredator 等等。你的 Animal 甚至可以給 IMovable 預設實作(像 Move(int speed)),但像 Lion 或 Fish 這些具體類別會自己改寫這個方法。


public interface ISaveable
{
    void SaveState(string path);
    bool HasChanges { get; }
}

public class Vector3
{
    public float X, Y, Z;
    public Vector3(float x, float y, float z) { X = x; Y = y; Z = z; }
    public override string ToString() => $"({X}, {Y}, {Z})";
}

public abstract class GameObject
{
    public string Id { get; }
    public Vector3 Position { get; }
    protected GameObject(string id, Vector3 pos) { Id = id; Position = pos; }
    public abstract void Update();
    public void Destroy() => Console.WriteLine($"{Id} 被銷毀");
}

public class Player : GameObject, ISaveable
{
    public int Health { get; private set; }
    private bool _hasChanges = true;
    public bool HasChanges => _hasChanges;

    public Player(string id, Vector3 pos, int health) : base(id, pos) => Health = health;

    public override void Update() =>
        Console.WriteLine($"{Id} 更新中,HP: {Health}, 位置: {Position}");

    public void TakeDamage(int dmg)
    {
        Health -= dmg;
        _hasChanges = true;
        Console.WriteLine($"{Id} 受到 {dmg} 傷害。HP: {Health}");
    }

    public void SaveState(string path)
    {
        Console.WriteLine($"儲存 {Id} (HP:{Health}) 到 {path}");
        _hasChanges = false;
    }
}

class GameEngine
{
    static void Main()
    {
        var player = new Player("P1", new Vector3(0, 0, 0), 100);
        player.Update();
        player.TakeDamage(20);

        ISaveable saver = player;
        if (saver.HasChanges) saver.SaveState("save.json");

        GameObject obj = player;
        obj.Update();
        player.Destroy();
    }
}

這個例子裡 Player 是 GameObject(因為繼承,拿到 Id、Position 和 Destroy 方法)。同時 Player 可以被儲存(因為實作 ISaveable 介面)。這種設計超彈性又強大!

7. 小細節與常見錯誤

想建立抽象類別實例:

Animal myAnimal = new Animal();
——這是新手最常犯的錯。記住:抽象類別只是模板,不是可以直接用的物件。編譯器會直接報錯。

忘記實作抽象方法: 如果你繼承抽象類別,而且你的類別不是抽象的,你一定要覆寫(override)所有基底類別的抽象方法。不然編譯器也會罵你。

想在介面加欄位: 這是介面最明確的「不行」。C# 8+ 雖然有靜態欄位,但實例欄位(屬於物件本身的)還是不能加。介面就是行為契約,不是資料倉庫。

搞混 virtual 跟 abstract:

  • virtual 方法/屬性有預設實作,可以被子類覆寫。
  • abstract 方法/屬性沒有實作,一定要在第一個非抽象子類覆寫。
  • 如果你用 virtual,但每個子類邏輯都不一樣,會變成大家都寫空方法再自己改,這樣契約就不明確,不如用 abstract。

搞懂這些差異,正確選擇抽象類別還是介面,這是每個 C# 工程師的基本功。這不只是語法問題,更是架構思維。設計系統時,問自己:「這些類別有共同的『是』階層嗎?」、「這些類別有共同的『能力』,但彼此沒關係嗎?」答案會幫你選對工具。

接下來的課我們會繼續深入介面的世界,學習 C# 新版帶來的進階功能。敬請期待!

2
任務
C# SELF, 等級 23, 課堂 1
上鎖
多媒體播放器的通用介面
多媒體播放器的通用介面
2
任務
C# SELF, 等級 23, 課堂 1
上鎖
為具有不同能力的物件建立介面
為具有不同能力的物件建立介面
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION