1. 從「純契約」到彈性架構
在 C# 8 之前,介面就像超嚴格的契約:你想實作介面——就得全部實作,連逗號都不能漏。如果介面多了新成員,所有現有的實作都得馬上加上,不然編譯器直接不讓你過。
但現實比這複雜。想像你維護一個被幾百個專案用的函式庫,突然你想在介面裡加個新方法。你不想破壞向後相容性吧?這時候 預設方法(Default Interface Methods, DIM)就登場啦!
重點是什麼?
預設方法讓你可以直接在介面裡宣告實作。現在契約變得更彈性:如果 class 沒實作「新功能」,就會用預設實作。這就像電影裡的替身:演員不想跳橋,特技演員來頂替。
2. Default Interface Methods 語法
怎麼在介面裡宣告有實作的方法?
其實跟一般方法很像,只是現在你可以(也必須)直接在介面裡寫方法本體:
public interface ILogger
{
void Log(string message);
// 新方法有預設實作!
void LogWarning(string message)
{
Log("[WARNING] " + message);
}
}
這裡 LogWarning 已經有實作啦!任何實作 ILogger 的 class 只需要實作 Log,LogWarning 會自動用預設的(除非你自己寫一個)。
比較一下:傳統 vs. 現代簽名
| 版本 | 介面宣告 |
|---|---|
| 到 C# 8 以前 | |
| C# 8 及以後 | |
語法重點
- 有實作的方法一定要用大括號寫方法本體。
- 預設方法不能是 abstract。
- 所有介面方法還是隱含 public。
- 也可以宣告有預設 get/set 的屬性(見下方)。
3. 實用範例
範例 1. 維持向後相容性
假設你的 app 有個儲存資料的介面:
public interface ISaveable
{
void Save(string filePath);
}
後來你想加個雲端儲存。不想改一堆 class?加個預設方法就好!
public interface ISaveable
{
void Save(string filePath);
// 新方法有「預設」實作!
void SaveToCloud(string cloudService)
{
Console.WriteLine($"儲存到雲端 {cloudService}(預設什麼都不做)");
}
}
現在所有舊的 class 自動「會」雲端儲存(雖然目前只會印訊息)。
範例 2. 擴展 logger 介面
之前我們有個簡單的 logger 介面:
public interface ILogger
{
void Log(string message);
}
加個預設方法來 log 錯誤:
public interface ILogger
{
void Log(string message);
void LogError(string message)
{
Log("[ERROR] " + message);
}
}
實作 ILogger 的 class 可以不用實作 LogError —— 會用預設的:
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
// LogError 不實作——會用預設的!
}
ILogger logger = new ConsoleLogger();
logger.Log("一切都 OK!");
logger.LogError("哎呀,出錯啦!"); // 會呼叫預設實作
範例 3. 預設方法 + 可擴展 app
你的 app 支援多種匯出:檔案、資料庫、網路。介面如下:
public interface IExporter
{
void Export(string data, string destination);
// 新功能——匯出到壓縮檔
void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("預設不支援壓縮。");
}
}
其他開發者寫的 plugin 也能繼續用,即使他們不知道新方法。
4. 預設介面方法怎麼被呼叫?
情境:「舊 class — 新介面」
如果 class 沒實作介面的預設方法,用介面參考呼叫時會用介面的實作。如果有實作——就用自己的。
public class FileExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("儲存到檔案...");
}
// ExportToArchive 不實作——會用預設的
}
IExporter exporter = new FileExporter();
exporter.Export("資料", "file.txt"); // 用 FileExporter 的實作
exporter.ExportToArchive("資料", "file.zip"); // 用預設實作!
情境:「class 覆寫預設方法」
public class AdvancedExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("進階模式儲存...");
}
public void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("支援壓縮!");
}
}
IExporter exporter = new AdvancedExporter();
exporter.ExportToArchive("資料", "file.zip"); // 會呼叫 class 的實作!
5. Default Interface Methods 還能做什麼?
預設屬性和事件
可以宣告有預設本體的屬性,只要有 get 或 set:
public interface IHasId
{
// 自動回傳 42,除非被覆寫
int Id => 42;
}
public class Person : IHasId {}
Console.WriteLine(new Person().Id); // 42
介面內呼叫預設方法
在介面裡,預設方法和其他成員可以互相呼叫:
public interface IDemo
{
void Foo() => Bar();
void Bar() => Console.WriteLine("BAR");
}
6. Default Interface Methods 限制與特性
能宣告欄位、建構子嗎?
不能。就算有預設方法,介面還是不是 class。不能有欄位、建構子、解構子。
能對介面用 base 嗎?
可以,但有細節。在介面的預設方法裡,可以用明確指定呼叫父介面的方法:
public interface IBase
{
void Greet() => Console.WriteLine("Hello from IBase");
}
public interface IDerived : IBase
{
void IBase.Greet()
{
Console.WriteLine("來自 IDerived 的問候!");
IBase.Greet(this); // 明確呼叫基底介面的方法
}
}
但這種情境在基礎用法裡很少見。
預設實作衝突時會怎樣?
public interface IA { void Foo() { Console.WriteLine("A"); } }
public interface IB { void Foo() { Console.WriteLine("B"); } }
// class 沒明確實作 Foo:
public class C : IA, IB { }
// 編譯錯誤:不知道要選哪個實作!
7. 常見錯誤、限制與特性
有趣的常見錯誤:
有些人學了 DIM 就想在介面裡宣告欄位——但還是不行。如果你想實作預設的 static 方法——C# 8 以前不行。(8 以後介面可以有 static 方法,但 static 預設實作又是另一回事。)
特性:Diamond Problem(菱形問題)
如果你的 class 實作兩個有同名預設方法的介面,你一定要明確實作這個方法:
public class ConflictClass : IA, IB
{
public void Foo() // 你要自己選要哪個!
{
// 明確用介面呼叫想要的實作(如果需要)
((IA)this).Foo();
// 或
((IB)this).Foo();
}
}
不要濫用!
預設方法能救向後相容性,但用太多會讓架構變髒,邏輯分散在介面裡。重要邏輯還是放 class,介面就當真的「契約」用吧。
GO TO FULL VERSION