[C#] 委托與事件(2)

網上講C#委托和事件的博文已經非常多了,其中也不乏一些深入淺出、條理清晰的文章。我之所以還是繼續寫,主要是借機整理學習筆記、歸納總結從而理解更透徹,當然能夠以自己的理解和思路給其他人講明白更好。
另外,太長的文章會讓很多讀者失去興趣,所以我決定把這篇分成四個部分來介紹。分別是委托的基礎、委托的進階、事件的基礎和事件的進階。對使用委托與事件要求不高的同學可以跳過進階部分。

本文接著講委托的高級知識,上一節請參見C#委托與事件(1)


4. 委托的高級知識

(1) 委托的類組成

前面我們介紹過了,委托實際上也是一個類,只不過它的對象不是一個普通的變量,而是一個方法。但是我們實際使用時并不需要定義這個類,而只是聲明一下委托即可。這是因為當C#編譯器處理委托類型時,會自動產生一個派生自System.MulticastDelegate的密封類,這個類與它的基類System.Delegate一起為委托提供必要的基礎設施。
下面我們就以上一節中聲明的委托delegate void GreetingDelegate(string s);來看看該類的組成。

public sealed class GreetingDelegate : System.MulticastDelegate
{
    public void Invoke(string s);
    public IAsyncResult BeginInvoke(string s, AsyncCallback cb, object state);
    public void EndInvoke(IAsyncResult result);
}

可以看到,該類中定義了三個公共方法:
(a) Invoke(),它被用來以同步方式調用委托對象維護的每個方法。所謂同步是指調用者必須等待調用完成才能繼續執行。Invoke()方法定義的參數和返回值完全匹配我們要定義的類GreetingDelegate。另外,Invoke()不能直接調用,而是在后臺調用。
(b) BeginInvoke(),用于異步調用,它最前面的參數列表是GreetingDelegate定義的方法類的參數列表,此外,還有兩個參數AsyncCallbackobject用于異步方法調用。
(c) EndInvoke(),與BeginInoke()聯合用于異步調用,它的返回值與委托聲明的返回值一致,而它的唯一參數則是BeginInvoke()返回的類型IAsyncResult接口。

另外,上面我們定義的委托沒有返回值,也可以定義返回值類型,這樣Invoke()EndInvoke()對應的返回值就不是void了。委托還可以指向包含任意數量out或者ref參數的方法,按道理只有Invoke()BeginInvoke()方法與委托的參數列表有關,需要加上相應的out或者ref參數,但是由于異步調用時需要通過EndInvoke()來返回結果,所以EndInvoke()的參數列表中需要加上out或者ref參數。

下面簡單介紹一下委托類的父類System.MulticastDelegate

public abstract class MulticastDelegate : Delegate
{
    // 返回所指向的方法列表
    public sealed override Delegate[] GetInvocationList();
    // 重載等于和不等于操作符
    public static bool operator ==(MulticastDelegate d1, MulticastDelegate  d2);
    public static bool operator !=(MulticastDelegate d1, MulticastDelegate d2);

    // 用來在內部管理委托所維護的方法列表
    private IntPtr _invocationCount;
    private object _invocationList;
}
public abstract class Delegate : IConeable, ISerializable
{
    // 與函數列表交互的方法
    // 給委托維護的列表添加一個方法,在C#中使用重載+=操作符調用此方法
    public static Delegate Combine(params Delegate[] delegates);
    public static Delegate Combine(Delegate a, Delegate b);
    // 從調用列表中移除一個或所有的方法,在C#中使用-=操作符調用此方法
    public static Delegate Remove(Delegate source, Delegate value);
    public static Delegate RemoveAll(Delegate source, Delegate value);

    // 重載操作符
    public static bool operator ==(Delegate d1, Delegate d2);
    public static bool operator !=(Delegate d1, Delegate d2);
 
    // 擴展委托目標的屬性
    public MethodInfo Method { get; } //用以表示委托維護的靜態方法的詳細信息
    public object Target { get; } // 如果方法調用是定義在對象級別的,
                                  // Target返回表示委托維護的方法的對象;
                                  // 如果調用的方法時一個靜態成員,
                                  // Target返回null
}
(2) 泛型委托

C#允許我們定義泛型委托,即當我們定義的委托接受的參數可能會不同時,我們可以通過類型參數來構建。下面我們來改寫一下上一節中定義的那個委托,使得它不僅支持傳入string,還支持傳入整型。

namespace TestDelegate
{
    delegate void GreetingDelegate<T>(T arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            // do something (hug or shake hand...)
        }

        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            // do something (hug or wave hand...)
        }

        public static void GreetingTimes(int n)
        {
            Console.WriteLine("  Greeting {0} times!", n); 
        }

        static void MakeGreeting<T>(T name, GreetingDelegate<T> greeting)
        {
            greeting(name);
        }

        static void Main(string[] args)
        {
            GreetingDelegate<string> d1 = Hello;        //定義委托的一個對象(將方法綁定到委托)
            d1 += Goodbye;                              // 在d1上再綁定一個委托
            GreetingDelegate<int> d2 = GreetingTimes;   //定義委托的另一個對象
            MakeGreeting("April", d1);
            MakeGreeting(99, d2);
        }
    }
}

輸出內容:

  Hello, April!
  Goodbye, April!
  Greeting 99 times!

有了泛型委托,很多方法都可以用一個委托模板表示出來,因此C#中提供了兩個常用的泛型委托Action<T>Func<T>來避免用戶手工構建自定義委托的麻煩。

Action<T>

Action<T>泛型委托定義的方法,參數列表可以多至16個(使用時需要指定各個參數的類型),返回值為void
例如,我們有一個方法為static void DisplayMessage(string msg, ConsoleColor txtColor, int printCount),若把它作為Action<T>委托的一個目標,則委托的實例化時需要這樣寫:
Action<string, ConsoleColor, int) actionTarget = new Action<string, ConsoleColor, int>(DisplayMessage);
調用委托方法時:
actionTarget("your input string", ConsoleColor.Green, 2);

Func<T>

Func<T>泛型委托也可以指向多至16個參數的方法,但與Action<T>不同的是,它具有自定義的返回值,具體用法類似,不再贅述。

(3) 委托的異步調用

在說委托的異步調用之前,我們先對第一節最早的delegate例子做一個簡單的改進,看看它的工作流程。首先,MakeGreeting()方法中除了調用greeting()之外,需要再調用一個額外的方法FuncAfterGreeting()。然后,我們定義了GreetingDelegate的一個對象d1,并在d1上綁定了Hello()Goodbye()方法。最后我們調用MakeGreeting()方法來看看輸出結果。

namespace AsyncDelegateTest
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 1 second");
            Thread.Sleep(1000);
            Console.WriteLine("  Finished Hello");
        }

        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            Console.WriteLine("  Waiting for 2 second");
            Thread.Sleep(2000);
            Console.WriteLine("  Finished Goodbye");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            greeting(name); // 這里相當于是greeting.Invoke(name);
            FuncAfterGreeting();
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     //定義委托的一個對象(將方法綁定到委托)
            d1 += Goodbye;                  //定義委托的另一個對象
            MakeGreeting("April", d1);
        }
    }
}

輸出結果:

  Hello, April!
  Waiting for 1 second
  Finished Hello
  Goodbye, April!
  Waiting for 2 second
  Finished Goodbye
  Do some other things...
  Waiting for 3 second
  Finished FuncAfterGreeting

從輸出我們可以看到MakeGreeting()方法中,首先調用了Hello()方法,并運行完畢;然后調用了Goodbye()方法,并運行完畢;最后調用FuncAfterGreeting(),并運行完畢;至此,整個MakeGreeting()方法運行完畢。
這就是采用同步的方式調用委托,這樣委托對象綁定的每個方法要依次執行,而且后者必須等前者執行完畢之后才能開始執行。另外,只有把委托對象綁定的所有方法執行完畢后才能回到MakeGreeting()方法中繼續往下執行。

而在(1)中我們介紹的BeginInvoke()EndInvoke()函數能使委托實現異步調用,所謂異步調用,就是在上例中MakeGreeting()方法中的線程去執行greeting方法時利用線程池中的線程去實現調用,自己則繼續往下執行。有了BeginInvoke()EndInvoke()這兩個函數后,異步調用就很簡單了,直接先用greeting調用BeginInvoke()函數,然后就可以做其他的事情,結束之間再調用EndInvoke()即可。

namespace AsyncDelegateTest2
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished Hello");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            IAsyncResult result = greeting.BeginInvoke(name, null, null);
            FuncAfterGreeting();
            greeting.EndInvoke(result);
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     //定義委托的一個對象(將方法綁定到委托)
            MakeGreeting("April", d1);
        }
    }
}

輸出結果:

  Do some other things...
  Hello, April!
  Waiting for 3 second
  Waiting for 3 second
  Finished Hello
  Finished FuncAfterGreeting

為什么我這里把Goodbye方法去掉了,這是因為BeginInvoke()只能在綁定了單個方法的delegate上調用,如果我們在d1上還綁定了其他方法,那么去調用BeginInvoke()的時候會出現下面的異常:

Unhandled Exception: System.ArgumentException: The delegate must have only one target.

當然如果你一定要綁定多個方法這樣用的話,可以先通過GetInvocationList()獲得綁定的方法列表,然后依次調用BeginInvoke()方法。

namespace AsyncDelegateTest3
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished Hello");
        }

        public static void Goodbye(string s)
        {
            Console.WriteLine("  Goodbye, {0}!", s);
            Console.WriteLine("  Waiting for 2 second");
            Thread.Sleep(2000);
            Console.WriteLine("  Finished Goodbye");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            Delegate[] delArray = greeting.GetInvocationList();
            foreach (var d in delArray)
            {
                var del = (GreetingDelegate)d;
                IAsyncResult result = del.BeginInvoke(name, null, null);
            }
            FuncAfterGreeting();
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;     // 定義委托的一個對象(將方法綁定到委托)
            d1 += Goodbye;                  // 定義委托的另一個對象
            MakeGreeting("April", d1);
            Console.ReadLine(); // 如果不加這行的話,很可能Hello方法還沒執行完程序就退出了,
                                // 因為我們沒有調用EndInvoke()去檢查它們的狀態
        }
    }
}

輸出結果:

  Do some other things...
  Hello, April!
  Goodbye, April!
  Waiting for 2 second
  Waiting for 3 second
  Waiting for 3 second
  Finished Goodbye
  Finished FuncAfterGreeting
  Finished Hello

回到AsyncDelegateTest2,這里其實還存在一個問題。那就是MakeGreeting()方法中的這句話greeting.EndInvoke(result);,如果Hello()方法需要執行30s,那么3s后FuncAfterGreeting()方法就執行完畢了,主線程執行到EndInvoke()這句話。而這句話就相當于讓主線程一直去查詢Hello()方法是否執行完畢。那么問題來了,能不能不要這么麻煩主線程,而是讓Hello()方法執行完畢后自動告訴主線程呢?這就是異步回調。

namespace AsyncDelegateTest4
{
    delegate void GreetingDelegate(string arg);
    class Program
    {
        public static void Hello(string s)
        {
            Console.WriteLine("  Hello, {0}!", s);
            Console.WriteLine("  Waiting for 5 second");
            Thread.Sleep(5000);
            Console.WriteLine("  Finished Hello");
        }

        public static void FuncAfterGreeting()
        {
            Console.WriteLine("  Do some other things...");
            Console.WriteLine("  Waiting for 3 second");
            Thread.Sleep(3000);
            Console.WriteLine("  Finished FuncAfterGreeting");
        }

        static void MakeGreeting(string name, GreetingDelegate greeting)
        {
            IAsyncResult result = greeting.BeginInvoke(name, new AsyncCallback(FuncForCallBack), "AsycState:OK");
            FuncAfterGreeting();
        }

        static void FuncForCallBack(IAsyncResult result)
        {
            // AsyncResult should using System.Runtime.Remoting.Messaging
            GreetingDelegate handler = (GreetingDelegate)((AsyncResult)result).AsyncDelegate;
            handler.EndInvoke(result);
            Console.WriteLine(result.AsyncState);
        }

        static void Main(string[] args)
        {
            GreetingDelegate d1 = Hello;
            MakeGreeting("April", d1);
            Console.ReadLine();
        }
    }
}

輸出結果:

  Do some other things...
  Hello, April!
  Waiting for 5 second
  Waiting for 3 second
  Finished FuncAfterGreeting
  Finished Hello
  AsycState:OK

這里我們定義了一個回調函數FuncForCallBack(),這樣就不需要在MakeGreeting()方法的最后顯示地去調用EndInvoke()去檢查委托方法的執行狀態了。

參考文獻:
《精通C#》
張子陽的《C# 中的委托和事件》
Delegates Tutorial
也來說說C#異步委托
C#委托的異步調用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容