1. 什麼是介面?
如果說抽象類別是有部分實作的「半成品」模板,那麼介面就是一份純粹的需求清單:某個東西(類別)必須要會什麼,才能在某個抽象情境下被用起來。
在寫程式時,介面就像是一份「合約」或「需求清單」,規定物件要有什麼行為。它描述了一組公開的方法、屬性、索引子和事件,實作這個介面的類別一定要提供這些東西。介面就像在說:「如果你想叫做 IDriveable(能開的),那你就要給我 Drive() 跟 Stop() 這兩個方法。」
你可以把介面想像成員工需求清單:比如你要找「廚師」,那介面上就會寫:「要會煮菜、上菜、洗手」。至於每個廚師怎麼做,隨便他,只要對外看起來都符合合約就好。
重點:介面只描述要有什麼,不管怎麼做。
用 OOP 術語簡單說
- 介面描述物件的外觀(API):可以做哪些事。
- 沒有狀態(沒有欄位)。傳統上介面也不能有實作,不過新版 C# 有例外(像預設方法),這個之後會細講。
- 一個類別可以實作無限多個介面(不像類別繼承只能一個!)。
生活中的比喻
USB 埠—大家都知道可以插滑鼠、鍵盤、隨身碟、耳機,甚至咖啡機(真的有這種東西!)。滑鼠裡面怎麼做不重要,只要有 USB 插頭、支援對的協定就行。這就是介面啦!
介面有什麼用?
- 降低耦合。程式只跟介面互動,不跟具體類別綁死。寫程式要「針對介面」而不是「針對實作」。
- 可以多重繼承行為:一個類別可以實作好幾個介面。
- 標準化:可以做通用機制:比如所有可以比較的物件都實作 IComparable。
- 好測試:測試時很容易用「假物件」取代真實實作。
- 好擴充:可以加新「模組」不用動到舊程式。
2. 介面宣告語法
來看程式碼吧!在 C# 裡面,介面用 interface 關鍵字宣告,名字通常都用 I 開頭:
// 介面宣告
public interface IPrintable
{
// 抽象方法—合約
void Print();
// 也可以宣告屬性
string Name { get; set; }
}
記住:介面的 method 跟 property 都沒有實作(沒有方法本體,只有簽名—跟抽象方法一樣)。
介面重點
- 不能在介面裡宣告欄位(成員變數)。
- 所有方法、屬性、事件、索引子預設都是 public(實作時也要是 public)。
- 介面不能有建構子(因為沒有狀態)。
- 介面不管實作它的類別是抽象還是具體都可以。
介面實戰
來把介面加到我們的學習小專案。假設我們現在有一個「可列印」介面(IPrintable),由 Report 類別實作,還有一個新類別 Invoice。
public interface IPrintable
{
void Print();
string Name { get; set; }
}
現在來定義一個實作這個介面的類別:
public class Report : IPrintable
{
public string Name { get; set; }
public Report(string name)
{
Name = name;
}
// 實作介面的方法
public void Print()
{
Console.WriteLine($"列印報告: {Name}");
}
}
再來一個完全不同的類別,但用同一個介面:
public class Invoice : IPrintable
{
public string Name { get; set; }
public Invoice(string name)
{
Name = name;
}
public void Print()
{
Console.WriteLine($"列印發票: {Name}");
}
}
現在我們可以寫一個方法,能處理任何可列印的東西:
public static void PrintAnything(IPrintable printable)
{
printable.Print(); // 就這樣!不管是報告還是發票,只要會列印就行。
}
來看一下怎麼用:
var report = new Report("每月報告");
var invoice = new Invoice("發票 #12345");
PrintAnything(report); // 列印報告: 每月報告
PrintAnything(invoice); // 列印發票: 發票 #12345
這樣介面就能讓你寫出通用、可擴充又漂亮的程式碼啦。
3. 介面實作
類別實作介面
類別用冒號來實作介面(對,跟繼承一樣):
public class Ticket : IPrintable
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine($"列印票券: {Name}");
}
}
重點:類別一定要實作介面的所有成員,而且要 public。
如果漏掉任何一個成員:
public class BrokenTicket : IPrintable
{
// 少了 Print() 的實作
public string Name { get; set; }
}
// 編譯錯誤: 'BrokenTicket' 沒有實作介面成員 'IPrintable.Print()'
多個介面
類別可以用逗號實作多個介面:
public interface IStorable
{
void Store();
}
public class MultiPurposeDoc : IPrintable, IStorable
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine("列印文件");
}
public void Store()
{
Console.WriteLine("儲存文件");
}
}
4. 介面有什麼用?
你現在可能在想:「好啦,語法我懂了。那這些東西在現實世界到底有什麼用,除了讓我這堂課更難過?」我跟你說,介面是專業開發裡最常用的工具之一。
職責分離與低耦合(Decoupling / Loose Coupling):
- 想像你在寫一個音樂播放器。它不在乎音樂從哪來—本地檔案、網路還是 CD,只要來源能給它音訊串流就好。
- 你可以定義一個 IAudioSource 介面,裡面有 GetAudioStream() 方法。
- 然後你會有 FileAudioSource、InternetAudioSource、CDAudioSource 這些類別來實作這個介面。
- 你的播放器只跟 IAudioSource 互動,不知道也不在乎具體是哪一種。如果明天又有新來源,比如 BluetoothAudioSource,你也不用改播放器的程式碼!只要寫一個新類別實作 IAudioSource 就好。這樣你的系統就超彈性又好擴充。這就是低耦合—元件只依賴抽象(介面),不依賴具體實作。
多型與統一處理:
就像我們剛剛 PrintAnything 的例子,你可以有一堆不同型別的物件,但它們都有共同的行為(介面描述的)。你可以對所有這些物件呼叫同一個方法(Print()),不用管它到底是報告、發票還是票券。這樣就能寫出很精簡又通用的程式碼。
測試(Unit Testing):
這大概是介面最重要的應用之一。你在測試某個元件時,常常會需要其他元件來配合(比如一個存資料的類別,會依賴一個操作資料庫的類別)。
你不用真的傳一個 DatabaseSaver(那你測試還要真的有資料庫!),你可以傳一個「假的」(或「mock」)物件,只要它有實作 IDataSaver 介面就行。這個「mock」物件只會假裝有存資料,根本不會去動真的資料庫。這樣你就能把元件分開測,速度快又不會被外部東西卡住。
API 跟框架開發:
你在寫函式庫或框架時,會想給開發者「擴充點」。介面超適合這種情境。你可以說:「如果你想讓你的元件跟我的系統合作,就實作這個介面吧」。.NET 標準函式庫裡面一堆介面(像 IEnumerable<T>、IDisposable、IComparable<T>)—它們就是為了定義最常見的合約。
直接針對介面寫程式(Programming to an Interface):
老手常說:「寫程式要針對介面,不要針對實作」。意思就是你在定義變數型別或方法參數時,盡量用介面(IDriveable),不要用具體類別(Car)。這樣你的程式碼就更彈性,也不會被實作細節綁死,想換一個實作也很容易。
5. 用介面時常見錯誤
錯誤一:想要 new 一個介面。
你可以寫 Cat murzik = new Cat("Murzik", 3);,因為 Cat 是具體類別。但你不能寫 ITalkable talker = new ITalkable();。介面只是合約、模板,沒有實作,不能直接 new。這就像藍圖,不是現成的房子。
錯誤二:忘記實作所有介面成員。
如果你說你的類別要實作介面,比如 IMyInterface,那你一定要實作它所有的方法。只要漏一個,編譯就會報錯:MyClass 沒有實作 IMyInterface.TheMissingMethod()。
錯誤三:實作時用錯存取修飾詞。
介面的方法預設就是 public,你實作時也一定要是 public。如果你寫成 private 或 protected,編譯器會報錯。你答應了就要公開實作。
錯誤四:想在介面裡加欄位或建構子。
介面只描述行為,不描述狀態。所以不能加欄位或建構子。你如果硬加就會編譯錯誤。只能有屬性,而且也只是描述 getter/setter。
錯誤五:把 override 跟介面實作搞混。
override 是拿來覆寫基底類別的方法。實作介面時不用寫 override—你只要寫一個 public 方法,簽名對就好。這個細節很容易搞錯。
GO TO FULL VERSION