此篇內容原文來自 C# in Depth 3rd Edition 翻譯及讀書心得
2.1 Delegate
大家都聽過 Delegate 這個名詞,但會發現實際在解釋 Delegate 的觀念時,會覺得沒有辦法精確的表達 Delegate 的精神。如果你很熟悉 C 語言並想試著解釋 Delegate 這個概念時,大概都會選用 function pointer 這個名詞來解釋。本質上來說,Delegate 提供了一種「間接處理」的概念,指定某些需立即執行的行為,並將這一系列的行為封裝到一個物件裡,當該物件被使用時,就可以執行被封裝在物件裡的行為。換個方式說明,你可以將 delegate type 視為一個方法的「介面」,那 delegate instance 就是實作介面的物件。如果這種解釋方式還是讓你覺得困惑,那在看看接下來的舉例,也許例子不是太討喜,但卻可以幫助我們更了解 delegate,想想看你會在你的遺囑裡列出什麼代辦事項,像是:
- 付帳單
- 捐款給慈善基金會
- 然後將剩餘的遺產留給你的貓
最後,在你離世之前,你會先寫好這些代辦事項,然後放在一個安全的地方,直到你離世後,你的律師會代替你執行遺囑裡的交辦事項。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 運作得宜,我們需要確保以下四個條件都滿足:
- 宣告 delegate type(委派型別)
- 你想要執行的程式碼,必須先包在一個方法裡
- 產生 delegate instance(委派實體)
- delegate instance 透過 Invoke() 方法執行
delegate type 是一種型別,定義了方法所需的參數列表,以及這個方法所回傳的型別,讓我們來看一下以下這個範例:
delegate void StringProcessor(string input);
這段程式碼意味著如果你想要建立一個 StringProcessor 這個類別的實體,你將會需要一個方法,這個方法需要傳入一個 string 參數以及沒有回傳值。
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 語法呼叫這個物件上的方法。
執行 delegate instance 上的方法相對容易,只需呼叫 Invoke 方法即可。
//// 這是我們寫的
proc1("Hello");
//// 建置時期會變成
proc1.Invoke("Hello");
//// 執行時期會呼叫
PrintString("Hellp");