[C#] 委派 Delegate

  • 4174
  • 0
  • 2019-04-26

此篇內容原文來自 C# in Depth 3rd Edition 翻譯及讀書心得

2.1 Delegate

大家都聽過 Delegate 這個名詞,但會發現實際在解釋 Delegate 的觀念時,會覺得沒有辦法精確的表達 Delegate 的精神。如果你很熟悉 C 語言並想試著解釋 Delegate 這個概念時,大概都會選用 function pointer 這個名詞來解釋。本質上來說,Delegate 提供了一種「間接處理」的概念,指定某些需立即執行的行為,並將這一系列的行為封裝到一個物件裡,當該物件被使用時,就可以執行被封裝在物件裡的行為。換個方式說明,你可以將 delegate type 視為一個方法的「介面」,那 delegate instance 就是實作介面的物件。如果這種解釋方式還是讓你覺得困惑,那在看看接下來的舉例,也許例子不是太討喜,但卻可以幫助我們更了解 delegate,想想看你會在你的遺囑裡列出什麼代辦事項,像是:

  1. 付帳單
  2. 捐款給慈善基金會
  3. 然後將剩餘的遺產留給你的貓

最後,在你離世之前,你會先寫好這些代辦事項,然後放在一個安全的地方,直到你離世後,你的律師會代替你執行遺囑裡的交辦事項。Delegate 在 C# 的語言裡面所做的事,就相當於剛剛遺囑裡的交辦事項,當我們在撰寫程式時,透過 Delegate 就可以讓我們在某個適當的時間點,執行一連串我們想要執行的動作。

當一段程式碼要執行某個動作,但該段程式碼對接下來要執行的動作內容一無所知時,就是一個典型適用 Delegate 的場景,我們用 Thread 這個類別來當做範例,首先我們先看一下 Thread 類別的建構式:

public Thread(
    ThreadStart start
)

//// ThreadStart 本身是個 delegate type:
public delegate void ThreadStart()

class Work 
{
    //// 實作 ThreadStart 簽章的方法
    public static void DoWork() 
    {
        Console.WriteLine("Static thread procedure."); 
    }
}

接下來的範例,示範如何建立 ThreadStart Delegate

ThreadStart threadDelegate = new ThreadStart(Work.DoWork);
Thread newThread = new Thread(threadDelegate);
newThread.Start();

從這個範例我們可以知道,當我們在初始化一個執行序的物件後,該物件所要執行的動作是由 ThreadStart 這個委派型別的參數決定,而這個參數我們已經綁訂定了 Work.DoWork 這個方法,所以當 newThread 在執行動作前,我們已經事先把預定要做的代辦事項先設定好,只要等到 newThread.Start() 一被呼叫,就會執行我們所預期的動作。

2.1.1 A recipe for simple delegates

為了確保 Delegate 運作得宜,我們需要確保以下四個條件都滿足:

  1. 宣告 delegate type(委派型別)
  2. 你想要執行的程式碼,必須先包在一個方法裡
  3. 產生 delegate instance(委派實體)
  4. delegate instance 透過 Invoke() 方法執行

delegate type 是一種型別,定義了方法所需的參數列表,以及這個方法所回傳的型別,讓我們來看一下以下這個範例:

delegate void StringProcessor(string input);

這段程式碼意味著如果你想要建立一個 StringProcessor 這個類別的實體,你將會需要一個方法,這個方法需要傳入一個 string 參數以及沒有回傳值。

當我們單看 Delegate 名詞時,有時會誤解,因為這個名詞常被同時用來代表 delegate type 或是 delegate instance,而這兩者的差異與常見的「類別」與「實體」的差異一樣,舉例來說:
Person person = new Person();
Person 本身是型別,而 person 是 Person 型別實體化的物件。在本章節裡,原作者都會明確使用 delegate type 與 delegate instance 確保讀者不會混淆。 

在我們定義好 StringProcessor 這個 delegate type 後,接下來就是要來找看看以下這五個方法,哪一個是符合 StringProcessor 的簽章條件:

void PrintString(string x)
void PrintInteger(int x)
void PrintTwoStrings(string x, string y)
int GetStringLength(string x)
void PrintObject(object x)

第一個方法完全符合,二三四都不符合,比較有意思的是第五個方法,當我們在 invoke 方法執行委派裡的動作時,其實執行 PrintObject 方法好像也合理,因為 string 也是繼承自 object 類別,但在 C# 1 版本裡,delegate 在參數的對應上,必須是一模一樣的型別才可以,但到了 C# 2 的版本裡, 卻放寬了這個條件,想知道原因的讀者請詳閱本書的第五章節。第四個方法看起來雖然傳入的參數型別一致,但是回傳的型別不一致,這會導致 JIT 在判斷當一個方法被執行時,需不需要將回傳值留在 stack 記憶體裡,所以第四個方法是不可行的。

我們找出適合的方法對應 delegate type 後,接下來看看如何建立 delegate type 的實體,並且指定 delegate instance 在被 invoke 後所要執行的方法。建立 delegate instance 的語法有兩種,取決於方法是靜態方法、或是實體方法。

StringProcessor proc1, proc2;
proc1 = new StringProcessor(StaticMethods.PrintString);
InstanceMethods instance = new InstanceMethods();
proc2 = new StringProcessor(instance.PrintString);

當我們想要執行的動作是放在靜態方法裡時,此時我們就是用 proc1 的語法,當這個動作是屬於 instance method 時,我們就需要先建立一個物件,之後才能透過 Invoke 語法呼叫這個物件上的方法。

因為 proc2 這個 delegate instance 已經指定好 instance.Printing 的方法,所以如果 proc2 本身不會被 garbage collected,那 instance 這個物件也同樣的不會被 garbage collected,否則會造成 memory leak 的問題。

執行 delegate instance 上的方法相對容易,只需呼叫 Invoke 方法即可。

//// 這是我們寫的
proc1("Hello"); 

//// 建置時期會變成
proc1.Invoke("Hello"); 

//// 執行時期會呼叫
PrintString("Hellp");