陋室铭
永远也不要停下学习的脚步(大道至简至易)

函數指標的進化論(下)

作者:蔡學鏞

2003 年 11 月

Delegate

C# 也支援多型與反射,但是 C# 卻是使用 delegate 來實現多緒和回呼 (而不使用多型與反射)。delegate 是函數指標的改良品種。delegate 的效率應該比多型稍差,但是用起來更方便,且允許使用靜態方法。

C# 編譯器對 delegate 以及 event 提供了大量的語法甜頭 (syntactic sugar),這些語法甜頭並不符合一般觀念中的程式語法,所以往往讓許多初學者丈二金剛摸不著頭腦。後面會陸續揭露 C# 編譯器的這些內幕。

C# 不支援函數指標,所以不能使用下面的語法:

void (*pFnc)(int, double);

必須改用下面的語法來宣告 delegate:

delegate void MyDelegate(int p1, double p2);

而上面的語法,等於下面的效果:

//版本一
class MyDelegate : System.MulticastDelegate {
public MyDelegate(Object target, System.IntPtr)
{ ... }
public void virtual Invoke(int p1, double p2)
{ ... }
public virtual IasyncResult BeginInvoke(...)
{ ... }
public virtual void EndInvoke(...)
{ ... }
}

其實,也可以是:

//版本二
class MyDelegate : System.MulticastDelegate {
public MyDelegate(Object target, System.IntPtr)
{ ... } // IntPtr 和 pointer 無關,是 native int 的意思
public void virtual Invoke(int p1, double p2)
{ ... }
}

為了簡單起見,我們只討論版本二。請注意,不管版本一與版本二,都無法編譯成功,因為 C# 語言規定:只有 C# 編譯器可以直接製造出繼承自 MulticastDelegate 的類別,編程員不可以在 C# 原始碼中定義 MulticastDelegate 的衍生類別。換句話說,這樣的語法甜頭是強制的,非用不可,別無選擇。

補充說明,MulticastDelegate 繼承自 Delegate。微軟原本的意思是讓 Delegate 的衍生類別只能包裝一個方法,MulticastDelegate 的衍生類別可以包裝多個方法。但是後來發現這樣的設計有相當多缺點,所以乾脆讓所有的 delegate 都繼承自 MulticastDelegate。由於這樣重大的設計變更來得太晚,所以微軟不敢全面調整 .NET Framework,怕會因此出現 bug,所以沒有更動原先的程式庫,只有更動編譯器和文件。讀者可能會認為,為何不用 .NET 特有的 side-by-side execution 方式 (用來解決 DLL Hell),同時執行兩個不同版本的 dll?我認為,問題之一出在 MulticastDelegate 與 Delegate 是屬於 mscorlib.dll,這是絕對不能使用 side-by-side execution 的 dll。目前 (1.0 與 1.1) 雖然 MulticastDelegate 與 Delegate 都還存在,但是在未來的 .NET 版本可就難說了。

編譯器幫我們產生的建構子需要兩個參數,第一個是方法所屬的物件,第二個是方法在「Method」metadata table 中的位置。編程員當然不會知道這個位置是幾號 (但是編譯器知道),所以編程員無法直接使用此建構子。事實上,產生 delegate 對象的過程中充滿離奇,有許多語法甜頭,下面會一一解釋。

你可以用下面的方式,來產生一個非靜態方法的 delegate:

new MyDelegate(myObject.MyNonStaticMethod);

編譯器會自動調用 MyDelegate 建構子,第一個參數是 myObject,第二個參數是 MyNonStaticMethod 方法在「Method」metadata table中的位置。

你也可以用下面的方式,來產生一個靜態方法的 delegate:

new MyDelegate(MyClass.MyStaticMethod);

編譯器會自動調用 MyDelegate 建構子,第一個參數是 null,第二個參數是 MyStaticMethod 方法在 「Method」metadata table中的位置。

注意:不管是不是 static 方法,都必須符合 MyDelegate 的 signature (參數和返回值的型態),否則編譯會失敗。

下面有更怪的例子:

MyDelegate md = null;
md += new MyDelegate(MyClass.MyStaticMethod);

第一次看到這樣的程式碼,許多人都會嚇了一跳:md 是 null,怎麼可以使用 +=?這會不會導致 System.NullReferenceException?事實上,這樣的寫法,編譯之後會變成:

MyDelegate md = null;
md = System.Delegate.Combine(md, new MyDelegate(MyClass.MyStaticMethod));

Combine() 是 System.Delegate 所提供的靜態方法,目的在於將第二個 Delegate 結合到第一個 Delegate 中,傳回此一新的 Delegate;如果第一個 Delegate 為 null,則直接傳回第二個 Delegate。

類似地,下面的程式:

md -= new MyDelegate(MyClass.MyStaticMethod);

編譯之後會變成:

md = System.Delegate.Remove(md, new MyDelegate(MyClass.MyStaticMethod));

Remove() 是 System.Delegate 所提供的靜態方法,目的在於將第二個 Delegate 自第一個 Delegate 中移除,並傳回此一新的 Delegate。

稍早提到下面的定義:

delegate void MyDelegate(int p1, double p2);

會造成編譯器會自動產生下面的定義。

class MyDelegate : System.MulticastDelegate {
public MyDelegate(Object target, System.IntPtr)
{ ... } // IntPtr 和 pointer 無關,是 native int 的意思
public void virtual Invoke(int p1, double p2)
{ ... }
}

現在我們把焦點集中在 Invoke() 上,此方法的參數和返回值型態一定會和 delegate 相同,以此例來說,方法參數必須是 int,double,而傳出值必須是 void。

如何調用 delegate?相當簡單,請看下面的例子:

MyDelegate d = new MyDelegate(MyClass.MyStaticMethod);
d(1, 3.4);

delegate 其實還有許多有趣的主題,包括 System.Reflection.RuntimeMethodInfo 類別做了哪些事 (這個類別是 Undocumented,.NET 1.0 文件中沒有說明)、多個 delegate 如何串接、delegate 如何和反映機制合作......等,因為篇幅有限,我都不在本文章說明,請感興趣的讀者自行研究這些主題。

C# 的多緒

傳統的多緒使用函數指標當參數,C# 利用 delegate 來取代函數指標,所以當然也將 delegate 用在多緒上。下面是一個 C# 多緒的例子:

using System;
using System.Threading;
class SimpleThreadApp {
public static void WorkerThreadMethod() {
// ...
}
public static void Main() {
ThreadStart woker = new ThreadStart(WorkerThreadMethod);
Thread t = new Thread(worker);
t.start();
}
}

ThreadStart 是一個 delegate,由 System.Threading 所提供。這個程式應該不難理解,所以我不再解釋。

C# 的回呼

對於 C# 來說,事件來源可以使用下面的方式來定義:

public class YourButton {
public YourDelegate Click;
// ...
}

這麼一來,外面的程式如果想要註冊,用法如下:

yourButton.Click += new YourDelegate(MyClass.MyStaticMethod);

在 YourButton 類別定義「內」,如果想通知所有的事件傾聽者,只要用下面的程式碼即可:

Click();

糟糕的是,連在 YourButton 類別定義「外」,也可以使用下面的方式,來產生通知,這樣子會違反物件導向的封裝精神。

yourButton.Click();

所以顯然我們應該將 YourButton 內的 Click 由 public 改成 private:

public class YourButton {
private YourDelegate Click;
// ...
}

但是這樣卻造成外面的程式無法向 YourButton 註冊,所以我們再將程式改成下面的模樣:

//作法一
public class YourButton {
private YourDelegate Click;
public void add_Click(YourDelegate d) {
Click += d;
}
public void remove_Click(YourDelegate d) {
Click -= d;
}
// ...
}

幾乎大家都有這樣的需求,所以 C# 編譯器於是又提供了一個語法甜頭 (利用 event 關鍵字),只要寫出下面 (作法二) 的程式,編譯之後的結果就和上面 (作法一) 一樣:

//作法二
public class YourButton {
public event YourDelegate Click;
// ...
}

或者你想要自行提供 add 和 remove 內的程式碼也成 (可能是為了提供 side-effect 程式碼),如下所示 (有點類似 property 的語法):

//作法三
public class YourButton {
private YourDelegate _Click;
public event YourDelegate Click {
add {
// .. side-effect code here, if any
Click += value;
// .. side-effect code here, if any
}
remove {
// .. side-effect code here, if any
Click -= value;
// .. side-effect code here, if any
}
}
// ...
}

為何用作法三,不用作法一,因為作法三有使用 event 關鍵字,只要有使用 event 關鍵字 (包括作法二),就會使得編譯器將它記錄在「Event」Metadata Table 內。有沒有紀錄這個對於執行時的毫無影響,但是可以幫助編譯器等工具軟件判讀,來簡化原始碼。例如,使用作法一,無法用下面的方式來註冊以及取消註冊。

yourButton.Click += new YourDelegate(MyClass.MyStaticMethod);
yourButton.Click -= new YourDelegate(MyClass.MyStaticMethod);

但是,使用作法二和三,則可以用這種方式來註冊以及取消註冊。因為編譯器從「Event」Metadata Table 內發現 Click 是 event,所以只要程式中使用 +=,則自動編譯成 add_Click();使用 -=,則自動編譯成 remove_Click()。

結論

函數指標、多型、反映、delegate,彼此之間互有關連,也各有優缺點。從函數指標演化到 delegate 的這段過程中,我對於這些機制設計者的巧思益發感到敬佩。

posted on 2007-03-06 12:13  宏宇  阅读(772)  评论(0编辑  收藏  举报