1. 為什麼要實作多個 interface?
當你在設計一個真實系統時,物件通常不只扮演一個「角色」,而是同時扮演好幾個。想像一下:你有一本電子書,不只能閱讀,還能編輯,甚至可以存到雲端。用物件導向程式設計(OOP)的術語來說,就是這個物件要同時實作好幾個 interface:
- IReadable — 讀取內容。
- IWritable — 編輯內容。
- ISyncable — 跟遠端儲存同步。
這時候多個 interface 的實作就派上用場啦!這是 C#(還有 OOP 整體)的一個超能力,class 沒有這功能:你不能同時繼承兩個以上的 class,但 interface 你想實作幾個都可以。
生活中的比喻
想像公司裡的一個員工。Vasya 可以同時是:
- 工程師(寫 code)
- 測試員(偶爾檢查別人的 code)
- 經理(規劃 sprint 或一天要喝幾杯咖啡)
這些「角色」的職責完全不同。但 Vasya 都能搞定每一份「合約」!寫程式也是一樣:class 實作 interface,然後就要負責它們的「職責」。
2. 多個 interface 的實作語法
超簡單:宣告 class 時用逗號分隔就好:
public interface IReadable
{
void Read();
}
public interface IWritable
{
void Write(string text);
}
public class Note : IReadable, IWritable
{
private string content = "";
public void Read()
{
Console.WriteLine("備忘錄: " + content);
}
public void Write(string text)
{
content = text;
Console.WriteLine("備忘錄已更新!");
}
}
這個例子裡,Note class 同時實作了兩個 interface。所以它一定要有兩個 method 的實作:Read 跟 Write。
3. 用「我們」的 app 當例子
在之前的例子裡,我們做了一個簡單的銀行 app 來處理帳戶。現在假設:我們需要一個通用的文件 class,可以列印(IPrintable)、存檔(ISavable),還有可能寄到 e-mail(IEmailable)。
先定義 interface:
public interface IPrintable
{
void Print();
}
public interface ISavable
{
void Save(string filePath);
}
public interface IEmailable
{
void Email(string toAddress);
}
一次全包的 class:
public class Statement : IPrintable, ISavable, IEmailable
{
public string Content { get; set; }
public void Print()
{
Console.WriteLine("列印對帳單...");
Console.WriteLine(Content);
}
public void Save(string filePath)
{
// 用標準函式庫的 File.WriteAllText。
File.WriteAllText(filePath, Content);
Console.WriteLine($"對帳單已存到檔案: {filePath}");
}
public void Email(string toAddress)
{
Console.WriteLine($"對帳單已寄到信箱: {toAddress}(模擬)");
}
}
這個 class 現在可以隨便用在 code 裡:
var stat = new Statement { Content = "本月操作:+1000 單位,-500 單位。" };
stat.Print();
stat.Save("statement.txt");
stat.Email("boss@bank.corp");
順帶一提,實務上很方便把這種物件傳給只需要某個 interface 的 method,不用管它還有什麼功能。比如列印的 method 只要 IPrintable,根本不用知道裡面還有「存檔」或「寄信」。
4. 用 interface reference:我的物件會什麼功能,外面看得到嗎?
這裡開始有點魔法。你實作了多個 interface 之後,可以只把物件當成其中一個「合約」來用。比如:
IPrintable printable = new Statement { Content = "抽象化講座" };
printable.Print(); // 只能列印
// printable.Save("file.txt"); // 錯誤:IPrintable interface 不知道 Save。
但如果你切換到另一個 interface:
ISavable savable = printable as ISavable;
if (savable != null)
{
savable.Save("file.txt");
}
這很方便,當你把物件傳給只需要某個 interface 的 method。這種做法可以降低耦合,讓 code 超靈活:你可以加新實作,不用改舊 code。
5. 多個 interface 實作時,不同 interface 有同名 method 怎麼辦?
這裡就有趣了!如果兩個 interface 都要一個同名 method,但意思不一樣怎麼辦?比如,想像咖啡機的 interface:
public interface IStartable
{
void Start();
}
public interface IRunnable
{
void Start();
}
咖啡機可以是「可啟動的」(IStartable — 開始煮咖啡),也可以是「可運行的」(IRunnable — 進入工作狀態)。
預設(一般)實作:
public class CoffeeMachine : IStartable, IRunnable
{
public void Start()
{
Console.WriteLine("咖啡機兩種角色都啟動!");
}
}
這種情況下,一個 Start() 實作同時滿足兩個 interface。
如果要不同意思呢?
可以用明確 interface 實作:
public class CoffeeMachine : IStartable, IRunnable
{
void IStartable.Start()
{
Console.WriteLine("啟動:開始煮飲料!");
}
void IRunnable.Start()
{
Console.WriteLine("啟動:機器進入工作模式。");
}
}
這時候只能用 interface 變數才能呼叫特定實作:
CoffeeMachine cm = new CoffeeMachine();
IStartable startable = cm;
startable.Start(); // 啟動:開始煮飲料!
IRunnable runnable = cm;
runnable.Start(); // 啟動:機器進入工作模式。
// cm.Start(); // 編譯不過!Start 不能直接用 class 變數叫。
這招在 .NET 標準函式庫很常見——比如一個 class 同時實作多個不同 framework 的類似 interface,每個都要不同行為。
6. 多個 interface 實作 vs. 繼承的差異
| 功能 | Class(繼承) | Interface |
|---|---|---|
| 基底型別數量 | 只能一個 | 隨你加幾個 |
| 繼承 code | 可以(有基底實作) | 不行,只有簽名(除非有預設 method) |
| 儲存狀態 | 可以 | 不行 |
| 加新角色 | 不行(或很麻煩,要用 composition) | 超簡單,直接實作 interface |
| 適合… | 「實體」階層 | 「角色」/邏輯功能 |
7. 實務應用:「混合」物件
靠多個 interface 實作,你可以做出有獨特「角色」組合的 class,完全不用煩惱多餘的繼承。
比如我們的銀行 app——可以做一個 class,既能存自己資訊、又能驗證自己、還能列印——全部用不同 interface:
public interface IValidatable
{
bool Validate();
}
public class Check : IPrintable, ISavable, IValidatable
{
public string Data { get; set; }
public void Print()
{
Console.WriteLine("列印支票: " + Data);
}
public void Save(string filePath)
{
File.WriteAllText(filePath, Data);
Console.WriteLine("支票已存檔: " + filePath);
}
public bool Validate()
{
return !string.IsNullOrEmpty(Data);
}
}
這種做法讓你可以很輕鬆擴充架構,class 需要什麼角色就加什麼 interface。如果要加新「職責」——直接實作新 interface 就好。
8. 常見錯誤與陷阱
新手常犯的錯:忘記實作 interface 的所有成員。編譯器不會放過你——會丟錯誤,還會提示你少了什麼。這種錯誤很好懂:「Class 必須實作 interface 成員」。
第二個常見問題——搞混 method 的可見性。如果你是明確實作 method,那它不能用 class 變數叫——只能用 interface。
還有一個常見情境——重構:你改了 interface(比如加了 method),但沒更新所有實作。這會導致編譯錯。設計時盡量不要在很多 class 用了之後再改 interface。
GO TO FULL VERSION