介面在 C# 裡常被簡化成一句話:「它是規格,不是實作。」這句話方向沒錯,但如果只停在這裡,很多實際行為會很難理解,例如隱式實作、顯式實作、泛型介面、值型別轉成介面時的 boxing。從 CLR 角度把它看清楚,這些規則其實是一整套連續設計。
介面定義的是能力契約
介面可以定義方法、屬性、事件與索引器,但它不負責提供實例狀態。從設計意圖上看,介面描述的是「某個型別承諾能做什麼」,而不是「這個型別內部怎麼存資料」。
public interface IWorker
{
void Work();
string Name { get; }
}
這段只是在說: 任何實作 IWorker 的型別,都必須能工作,並且能提供名稱。它沒有說明欄位長怎樣,也沒有規定內部演算法。
在 CLR 眼裡,介面也是型別定義
雖然介面不能直接 new 出物件,但 CLR 仍然把它當成一種正式型別。這代表:
- 介面有自己的 metadata
- 可以透過反射取得它的方法與成員資訊
- runtime 需要維護「某個具體型別實作了哪些介面、每個介面成員對應到哪個方法」
也就是說,介面不是單純的編譯器語法概念,而是 runtime 真正理解的一級公民。
實作介面時,方法表映射才是關鍵
當一個型別實作介面時,真正重要的不只是「有沒有同名方法」,而是 CLR 必須知道這個介面的每個成員要映射到該型別的哪個方法。
public interface ITestInterface
{
void SealTest();
void VirtualTest();
}
public class TestConcreteClass : ITestInterface
{
public void SealTest() { }
public virtual void VirtualTest() { }
}
這裡 SealTest 和 VirtualTest 都能滿足介面契約,但兩者是否可被衍生類別覆寫,取決於類別本身是否把方法定義成 virtual,不是取決於介面。
也就是說:
- 介面只要求「必須有這個能力」
- 類別自己決定「這個能力能否再被覆寫」
隱式介面實作是最常見形式
最直覺的寫法,就是直接寫一個 public 成員來對應介面:
public interface IDisposableLike
{
void Dispose();
}
public sealed class SimpleType : IDisposableLike
{
public void Dispose()
{
Console.WriteLine("Dispose");
}
}
這種寫法稱為隱式介面實作。好處是:
- 類別使用者直接透過物件就能呼叫方法
- 介面參考也能呼叫同一個方法
- API 最直觀
var obj = new SimpleType();
obj.Dispose();
IDisposableLike disposable = obj;
disposable.Dispose();
這兩次呼叫最終都會落到同一個實作。
方法是否 virtual,影響的是類別繼承鏈,不是介面本身
如果類別把介面方法定義成 virtual,衍生類別就能覆寫:
public class BaseWorker : IWorker
{
public virtual void Work()
{
Console.WriteLine("Base Work");
}
public string Name => "Base";
}
public class DerivedWorker : BaseWorker
{
public override void Work()
{
Console.WriteLine("Derived Work");
}
}
若沒有 virtual,衍生類別即使寫出同名方法,也只是 new 出一個新成員,不是覆寫原本那個介面映射的方法。
這也是為什麼同一個物件,在「以類別型別看」和「以介面型別看」時,可能會出現不同呼叫結果。關鍵不是語法表面,而是 runtime 綁定到哪個介面實作槽位。
顯式介面實作用來隱藏或分流介面成員
除了隱式實作,C# 還支援 EIMI: Explicit Interface Method Implementation。
public sealed class SimpleType : IDisposable
{
public void Dispose()
{
Console.WriteLine("SimpleType Dispose");
}
void IDisposable.Dispose()
{
Console.WriteLine("IDisposable Dispose");
}
}
這時:
var obj = new SimpleType();
obj.Dispose(); // SimpleType Dispose
((IDisposable)obj).Dispose(); // IDisposable Dispose
顯式實作的特性很明確:
- 不能寫存取修飾詞
- 不能直接透過類別實例名稱呼叫
- 只能先轉成對應介面後再呼叫
- 通常視為 private 且不可再覆寫的介面對應實作
它的主要用途不是炫技,而是解決名稱衝突或故意把某些介面能力藏在介面視圖後面。
多個介面有同名成員時,顯式實作通常最乾淨
如果同一個類別同時實作兩個介面,而兩個介面有相同簽章的方法,顯式實作通常是最直接的做法:
public interface ITestA
{
void Test();
}
public interface ITestB
{
void Test();
}
public class Tester : ITestA, ITestB
{
void ITestA.Test()
{
Console.WriteLine("A");
}
void ITestB.Test()
{
Console.WriteLine("B");
}
}
呼叫時就必須先轉型:
var tester = new Tester();
((ITestA)tester).Test();
((ITestB)tester).Test();
這裡的重點不是「語法麻煩」,而是你明確告訴 runtime: 同一個類別可以對不同介面提供不同版本的實作。
介面映射是型別內部結構的一部分
當型別載入到 CLR 時,runtime 會建立這個型別的內部描述資料,其中包含它自己的方法,也包含它所實作介面的映射資訊。

可以把它理解成一種「介面方法到具體方法的對照表」。因此,介面呼叫不是單純比對名字,而是透過型別資訊找到正確的實作入口。這也是為什麼顯式實作、覆寫、隱藏等行為會有細微差異。
泛型介面把型別安全與效能一起拉上來
非泛型介面在早期很常見,例如 IComparable:
public interface IComparable
{
int CompareTo(object obj);
}
問題是這會帶來兩個成本:
- 編譯期型別資訊不足,呼叫端得自己保證傳對型別
- 值型別參與時,常常需要 boxing
因此現代 C# 更常用泛型介面,例如 IComparable<T>:
public sealed class Number : IComparable<int>, IComparable<string>
{
private readonly int _value = 5;
public int CompareTo(int other)
{
return _value.CompareTo(other);
}
public int CompareTo(string other)
{
return _value.CompareTo(int.Parse(other));
}
}
這種設計的好處很實際:
- 編譯器能檢查型別是否正確
- 一個型別可以針對不同型別提供不同比較邏輯
- 值型別使用時,更容易避免不必要 boxing
介面約束讓泛型方法只接受有特定能力的型別
介面除了拿來做多型,也常被拿來當泛型約束:
void M<T>(T value) where T : IComparable, IConvertible
{
}
這表示只有同時實作 IComparable 與 IConvertible 的型別才能傳進來。
int x = 5;
M(x); // OK
Guid g = Guid.NewGuid();
// M(g); // 編譯失敗
這種設計把「能力需求」提早到編譯期表達,而不是等執行時再丟例外。
介面約束還能減少值型別 boxing
這是介面約束很容易被忽略的一個實際收益。比較下面兩種寫法:
void N(IComparable value)
{
}
void M<T>(T value) where T : IComparable
{
}
若你把 int 傳進 N,因為參數型別是介面參考,int 通常必須先 boxing 才能被當成 IComparable 傳遞。
但 M<T> 不一樣。編譯器和 JIT 知道 T 必須實作 IComparable,卻不必先把它裝箱成介面物件再呼叫,因此在很多情況下能保留值型別語意,減少額外配置。
這也是泛型介面與泛型約束常被視為「不只更型別安全,也更有效率」的原因。
什麼時候該用介面,什麼時候不該硬上
適合用介面的場景:
- 你要表達的是能力契約,而不是共用狀態
- 多個不相干型別都需要提供同一種操作
- API 呼叫端應該依賴抽象,而不是依賴具體類別
不適合硬塞介面的場景:
- 根本不需要多型,只是為了「看起來有架構」
- 型別之間其實有大量共享實作,抽成基底類別更自然
- 介面拆得太碎,導致呼叫方反而更難理解整體模型
介面是抽象工具,不是架構裝飾品。若沒有清楚的多型需求,介面越多通常只會讓系統更難讀。
實務上該記住什麼
- 介面描述的是能力契約,不是物件狀態。
- CLR 真的會追蹤介面到具體方法的映射,不只是編譯器表面規則。
- 隱式實作最直觀;顯式實作用來處理衝突或刻意隱藏介面成員。
- 泛型介面比非泛型介面更型別安全,也更容易避免 boxing。
- 介面約束能把能力需求提早到編譯期,並改善值型別傳遞效率。
參考資料
- 《CLR via C#》 Jeffrey Richter