CodeGym /課程 /C# SELF /C# 中的明確介面實作

C# 中的明確介面實作

C# SELF
等級 23 , 課堂 4
開放

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 用的。

解決名稱衝突

來看剛剛那個 IWriterIPrinter 的例子。我們繼續寫一個報表 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 裡。可以想像成這樣的表格:

怎麼呼叫 實際會執行什麼
report.Print()
編譯錯誤 — 沒有這個方法
((IWriter)report).Print()
明確實作
IWriter.Print()
((IPrinter)report).Print()
明確實作
IPrinter.Print()
report.Show()
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,只能透過介面用。
  • 這些成員不能是 staticvirtualabstractoverride
  • 不能直接用 class 物件呼叫明確實作的成員(只能用介面 reference)。
  • 如果介面繼承其他介面,可以明確實作任何層級的成員。

7. 明確實作的好處

明確實作不只是語法小技巧來解決衝突。它有幾個很重要的理由:

解決名稱衝突(The Big One): 這是最主要、最明顯的理由。如果你同時實作兩個介面,它們有同樣簽名的方法(或屬性),明確實作可以讓你給每個介面不同的實作。不然就會有不明確的問題。
真實世界例子: 想像你有一台印表機,它同時也是掃描器。IPrinterPrint()IScannerScan()。但如果兩個介面都有 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)。
語法
public ReturnType MethodName(Params) { ... }
ReturnType InterfaceName.MethodName(Params) { ... }
解決衝突 不能解決,方法會給所有有這個簽名的介面用。 可以解決衝突,給每個介面不同實作。
API 乾淨度 方法會是 class 的 public API。 方法不會是 class 的 public API。
用途 給一般、明確的介面實作。 給解決衝突或隱藏特殊邏輯用。

你看,這兩種方式各有用途,大部分時候你會用隱式實作,因為比較簡單方便。明確實作是給特殊但很重要的情境用的工具。

8. 明確實作介面時常見錯誤

錯誤 1:方法不能直接用 class 物件呼叫。
明確實作的方法不能直接用 class 實例呼叫。常常有人搞混,compiler 說找不到方法,其實只是「藏」起來了。解法:把物件轉成介面型別,例如:(IMyInterface)obj.Method()

錯誤 2:用錯存取修飾詞。
明確實作時不能加 publicoverride 這種修飾詞。這樣 compiler 會報錯,不接受這種宣告。

錯誤 3:想在子類別 override 明確實作的方法。
如果介面方法在基底 class 明確實作了,子類別不能 override。設計 class 和介面繼承時要注意這個限制。

1
問卷/小測驗
介面嘅概念,等級 23,課堂 4
未開放
介面嘅概念
介面:基礎同契約
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION