zoukankan      html  css  js  c++  java
  • 函数指针进化论(下)

    函數指標的進化論(下)

    作者:蔡學鏞

    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 的這段過程中,我對於這些機制設計者的巧思益發感到敬佩。

  • 相关阅读:
    解决mybatis xml文件代码提示
    SVN cleanup failed–previous operation has not finished; run cleanup if it was interrupted
    有36辆自动赛车和6条跑道,没有计时器的前提下,最少用几次比赛可以筛选出最快的三辆赛车?
    mybatis如何在控制台打印执行的sql语句
    Ionic2如何下拉刷新和上拉加载
    Ionic 如何把左上角的按钮去掉?
    Ionic1与Ionic2
    java的四种引用,强弱软虚
    equals变量在前面或者在后面有什么区别吗?这是一个坑点
    Java基础—复用类
  • 原文地址:https://www.cnblogs.com/cuihongyu3503319/p/665262.html
Copyright © 2011-2022 走看看