1. 前言
通常你在實作介面時,只要在你的 class 裡寫出 public 方法,簽名跟介面裡宣告的一樣就好。這叫做隱式或公開 (public) 實作。compiler 很聰明,會自動懂:「喔,這個 DoSomething() 方法在 MyClass 是要實作 IDoable.DoSomething()!」
但如果:
- 你的 class SmartDevice 同時實作 ICamera(有個 TakePicture() 方法)還有 IScreen(也有 TakePicture(),但是做截圖)?
- 或者你的 class Robot 已經有一個 public 的 Reset() 方法(全部重置),但你又想讓它實作 IDevice 介面,裡面的 Reset() 只重置部分設定?
這時就會有不明確或想要明確分開功能的需求。這時候就輪到明確介面實作出場啦。
明確介面實作 讓你可以跟 compiler 說:「這個方法就是要給 這個 介面用的,只有透過這個介面 reference 才能用到。」有點像彼得帕克只有在當蜘蛛人時才會噴蜘蛛絲,當「普通」彼得帕克時就是攝影師技能。
2. 什麼時候需要明確介面實作
你用一般方式實作介面時,方法和屬性都會變成 class 的 public API。但有時你會希望它們只能透過介面來用,不能直接用 class。這種情況比你想像的還常見!例如兩個介面都要同名的方法(但意義不同),或你想限制只有用 interface reference 才能用到某些功能。
這時明確介面實作就很有用了。這就像程式架構裡的祕密通道:外面看不到,內行人才知道怎麼進去!
情境 1:名稱衝突
想像一下,你有個 class 同時實作兩個介面,兩個都要一個同名但邏輯完全不同的方法。例如:
interface IWriter
{
void Print();
}
interface IPrinter
{
void Print();
}
你想讓 IWriter.Print() 把文字寫進檔案,而 IPrinter.Print() 則是送到印表機。直接寫一個 Print() 方法沒辦法分開行為。這時明確實作就能救你。
情境 2:隱藏不給用戶看的技術性介面方法
有時你的 class 必須實作某個介面的方法,但你根本不想讓所有 class 的使用者都看到(例如這個介面成員只給內部基礎設施用)。
情境 3:防止誤用
如果介面的方法不是給大家直接呼叫的(例如 framework 的 internal 機制),你可以明確實作,這樣其他工程師就不會不小心直接呼叫到。
3. 明確介面實作語法
重點是:明確實作時你要用完整的介面名稱來命名方法和屬性。不能加任何存取修飾詞(public/private)或 override!
基本語法:
回傳型別 介面名稱.方法名稱(參數)
{
// 實作內容
}
看起來就像你直接指定介面名稱,讓 compiler 和同事都不會搞混這個實作是給哪個 contract 用的。
解決名稱衝突
來看剛剛那個 IWriter 和 IPrinter 的例子。我們繼續寫一個報表 class:
interface IWriter
{
void Print();
}
interface IPrinter
{
void Print();
}
public class Report : IWriter, IPrinter
{
// 明確實作 IWriter.Print
void IWriter.Print()
{
Console.WriteLine("把報表存到檔案 (Writer)...");
}
// 明確實作 IPrinter.Print
void IPrinter.Print()
{
Console.WriteLine("把報表送到紙本印表機 (Printer)...");
}
// 額外的公開方法
public void Show()
{
Console.WriteLine("在螢幕上顯示報表。");
}
}
現在來用不同介面呼叫:
var report = new Report();
report.Show(); // 一般公開方法
// report.Print(); // 錯誤!Report 沒有 Print 這個方法
IWriter writer = report;
writer.Print(); // 會呼叫 IWriter.Print() 的實作
IPrinter printer = report;
printer.Print(); // 會呼叫 IPrinter.Print() 的實作
這裡 Print 方法不能直接用 report 物件呼叫,只能透過對應的介面。這就是明確實作的精髓:只有透過介面「port」才能進去。
記憶體裡長怎樣:簡單圖解
其實明確實作就是把介面成員「藏」在 class 裡。可以想像成這樣的表格:
| 怎麼呼叫 | 實際會執行什麼 |
|---|---|
|
編譯錯誤 — 沒有這個方法 |
|
明確實作 |
|
明確實作 |
|
class 的 Show 方法 |
4. 範例:介面只給基礎設施用
實務上很常遇到這種情境:class 必須實作某個服務用的介面,但一般用戶根本不需要看到這個實作。
interface IBroadcastable
{
void Broadcast();
}
public class SecretMessage : IBroadcastable
{
void IBroadcastable.Broadcast()
{
Console.WriteLine("祕密訊息已經發送到空中...");
}
public void Reveal()
{
Console.WriteLine("在螢幕上顯示祕密。");
}
}
// 一般程式碼:
var message = new SecretMessage();
message.Reveal(); // 用戶方法
// message.Broadcast(); // 錯誤!沒有這個方法
// 只有基礎設施知道怎麼用:
((IBroadcastable)message).Broadcast();
這裡 Broadcast() 方法只給知道 contract 的人才用。
5. 明確實作屬性和索引子
不只方法可以明確實作,屬性和索引子也可以。
interface IDescribable
{
string Description { get; }
}
public class Product : IDescribable
{
// 明確實作屬性
string IDescribable.Description => "只有透過介面才能看到的描述";
// 一般公開屬性
public string Name { get; set; }
}
// 使用範例:
var p = new Product { Name = "小玩意" };
// p.Description; // 錯誤!Product 沒有這個屬性
var descr = ((IDescribable)p).Description;
Console.WriteLine(descr);
6. 有用的小細節
明確實作遇到繼承時
繼承時其實很直覺:如果基底 class 明確實作了介面,子類別會繼承這個「行為」。但如果子類別想 override 這個介面方法,是不行的:明確實作不能是 virtual。也就是說,不能 override 明確實作的成員。
如果你真的需要這種行為——只能用一般實作,或考慮 template method pattern。
明確 vs. 隱式實作
+----------------+
| Invoice |
+----------------+
| Show() | // 可以直接呼叫
+----------------+
| ITxtExportable.Export() // 只能透過 ITxtExportable
| IJsonExportable.Export() // 只能透過 IJsonExportable
+----------------+
明確實作的特點/限制
- 明確實作的方法和屬性不能有存取修飾詞。它們預設對外部是 private,只能透過介面用。
- 這些成員不能是 static、virtual、abstract 或 override。
- 不能直接用 class 物件呼叫明確實作的成員(只能用介面 reference)。
- 如果介面繼承其他介面,可以明確實作任何層級的成員。
7. 明確實作的好處
明確實作不只是語法小技巧來解決衝突。它有幾個很重要的理由:
解決名稱衝突(The Big One): 這是最主要、最明顯的理由。如果你同時實作兩個介面,它們有同樣簽名的方法(或屬性),明確實作可以讓你給每個介面不同的實作。不然就會有不明確的問題。
真實世界例子: 想像你有一台印表機,它同時也是掃描器。IPrinter 有 Print(),IScanner 有 Scan()。但如果兩個介面都有 ProcessDocument()?明確實作就能讓 IPrinter.ProcessDocument() 做列印,IScanner.ProcessDocument() 做掃描,兩個完全不同。
隱藏實作細節、讓 class API 更乾淨: 明確實作的方法不會是 class 的 public API。只有把物件轉成介面型別才看得到。這很適合你想讓某些功能只給「有 contract」的人用,不想讓大家都能用。
例子: 你做一個複雜的金融工具,比如 CreditCard。它可以實作 IPayable(付款用)和 IAdminConfigurable(內部設定,例如設限額)。IAdminConfigurable.SetLimit() 不該讓每個人都能用,只給管理系統用,這時明確實作就能讓 SetLimit() 不會出現在 CreditCard 的一般 API 裡,讓 class API 更乾淨安全。
保證 contract: 有時你的 class 剛好有個跟介面同名同簽名的方法,但你不想讓它自動變成介面的實作。例如你有個 MyList class,有個 Clear() 方法清自己的狀態。你決定讓 MyList 實作 IList<T>,而 IList<T> 也有 Clear()。預設你的 MyList.Clear() 會變成 IList<T>.Clear() 的實作。如果兩者邏輯不同,明確實作 IList<T>.Clear() 就能分開。
隱式 (Implicit) vs. 明確 (Explicit) 實作
來看個對照表更清楚:
| 特性 | 隱式 (Implicit) 實作 | 明確 (Explicit) 實作 |
|---|---|---|
| 可用性 | class 和介面都能呼叫。 | 只能透過介面呼叫。 |
| 存取修飾詞 | 通常 public(或 protected 等)。 | 沒有存取修飾詞(不能是 public)。 |
| 語法 | |
|
| 解決衝突 | 不能解決,方法會給所有有這個簽名的介面用。 | 可以解決衝突,給每個介面不同實作。 |
| API 乾淨度 | 方法會是 class 的 public API。 | 方法不會是 class 的 public API。 |
| 用途 | 給一般、明確的介面實作。 | 給解決衝突或隱藏特殊邏輯用。 |
你看,這兩種方式各有用途,大部分時候你會用隱式實作,因為比較簡單方便。明確實作是給特殊但很重要的情境用的工具。
8. 明確實作介面時常見錯誤
錯誤 1:方法不能直接用 class 物件呼叫。
明確實作的方法不能直接用 class 實例呼叫。常常有人搞混,compiler 說找不到方法,其實只是「藏」起來了。解法:把物件轉成介面型別,例如:(IMyInterface)obj.Method()。
錯誤 2:用錯存取修飾詞。
明確實作時不能加 public 或 override 這種修飾詞。這樣 compiler 會報錯,不接受這種宣告。
錯誤 3:想在子類別 override 明確實作的方法。
如果介面方法在基底 class 明確實作了,子類別不能 override。設計 class 和介面繼承時要注意這個限制。
GO TO FULL VERSION