C#中的委託和事件2-2(轉)

引言

若是你看過了 C#中的委託和事件2-1 一文,我想你對委託和事件已經有了一個基本的認識。但那些遠不是委託和事件的所有內容,還有不少的地方沒有涉及。本文將討論委託和事件一些更爲細節的問題,包括一些你們常問到的問題,以及事件訪問器、異常處理、超時處理和異步方法調用等內容。html

爲何要使用事件而不是委託變量?

C#中的委託和事件 中,我提出了兩個爲何在類型中使用事件向外部提供方法註冊,而不是直接使用委託變量的緣由。主要是從封裝性和易用性上去考慮,可是還漏掉了一點,事件應該由事件發佈者觸發,而不該該由客戶端(客戶程序)來觸發。這句話是什麼意思呢?請看下面的範例:編程

NOTE:注意這裏術語的變化,當咱們單獨談論事件,咱們說發佈者(publisher)、訂閱者(subscriber)、客戶端(client)。當咱們討論Observer模式,咱們說主題(subject)和觀察者(observer)。客戶端一般是包含Main()方法的Program類。數組

class Program {
    static void Main(string[] args) {
        Publishser pub = new Publishser();
        Subscriber sub = new Subscriber();
       
        pub.NumberChanged += new NumberChangedEventHandler(sub.OnNumberChanged);
        pub.DoSomething();          // 應該經過DoSomething()來觸發事件
        pub.NumberChanged(100);     // 但能夠被這樣直接調用,對委託變量的不恰當使用
    }
}

// 定義委託
public delegate void NumberChangedEventHandler(int count);

// 定義事件發佈者
public class Publishser {
    private int count;
    public NumberChangedEventHandler NumberChanged;         // 聲明委託變量
    //public event NumberChangedEventHandler NumberChanged; // 聲明一個事件

    public void DoSomething() {
        // 在這裏完成一些工做 ...

        if (NumberChanged != null) {    // 觸發事件
            count++;
            NumberChanged(count);
        }
    }
}

// 定義事件訂閱者
public class Subscriber {
    public void OnNumberChanged(int count) {
        Console.WriteLine("Subscriber notified: count = {0}", count);
    }
}app

上面代碼定義了一個NumberChangedEventHandler委託,而後咱們建立了事件的發佈者Publisher和訂閱者Subscriber。當使用委託變量時,客戶端能夠直接經過委託變量觸發事件,也就是直接調用pub.NumberChanged(100),這將會影響到全部註冊了該委託的訂閱者。而事件的本意應該爲在事件發佈者在其自己的某個行爲中觸發,好比說在方法DoSomething()中知足某個條件後觸發。經過添加event關鍵字來發布事件,事件發佈者的封裝性會更好,事件僅僅是供其餘類型訂閱,而客戶端不能直接觸發事件(語句pub.NumberChanged(100)沒法經過編譯),事件只能在事件發佈者Publisher類的內部觸發(好比在方法pub.DoSomething()中),換言之,就是NumberChanged(100)語句只能在Publisher內部被調用。異步

你們能夠嘗試一下,將委託變量的聲明那行代碼註釋掉,而後取消下面事件聲明的註釋。此時程序是沒法編譯的,當你使用了event關鍵字以後,直接在客戶端觸發事件這種行爲,也就是直接調用pub.NumberChanged(100),是被禁止的。事件只能經過調用DoSomething()來觸發。這樣纔是事件的本意,事件發佈者的封裝纔會更好。async

就好像若是咱們要定義一個數字類型,咱們會使用int而不是使用object同樣,給予對象過多的能力並不見得是一件好事,應該是越合適越好。儘管直接使用委託變量一般不會有什麼問題,但它給了客戶端不該具備的能力,而使用事件,能夠限制這一能力,更精確地對類型進行封裝。異步編程

NOTE:這裏還有一個約定俗稱的規定,就是訂閱事件的方法的命名,一般爲「On事件名」,好比這裏的OnNumberChanged。性能

爲何委託定義的返回值一般都爲void?

儘管並不是必需,可是咱們發現不少的委託定義返回值都爲void,爲何呢?這是由於委託變量能夠供多個訂閱者註冊,若是定義了返回值,那麼多個訂閱者的方法都會向發佈者返回數值,結果就是後面一個返回的方法值將前面的返回值覆蓋掉了,所以,實際上只能得到最後一個方法調用的返回值。能夠運行下面的代碼測試一下。除此之外,發佈者和訂閱者是鬆耦合的,發佈者根本不關心誰訂閱了它的事件、爲何要訂閱,更別說訂閱者的返回值了,因此返回訂閱者的方法返回值大多數狀況下根本沒有必要。測試

class Program {
    static void Main(string[] args) {
        Publishser pub = new Publishser();
        Subscriber1 sub1 = new Subscriber1();
        Subscriber2 sub2 = new Subscriber2();
        Subscriber3 sub3 = new Subscriber3();

        pub.NumberChanged += new GeneralEventHandler(sub1.OnNumberChanged);
        pub.NumberChanged += new GeneralEventHandler(sub2.OnNumberChanged);
        pub.NumberChanged += new GeneralEventHandler(sub3.OnNumberChanged);
        pub.DoSomething();          // 觸發事件
    }
}

// 定義委託
public delegate string GeneralEventHandler();

// 定義事件發佈者
public class Publishser {
    public event GeneralEventHandler NumberChanged; // 聲明一個事件
    public void DoSomething() {
        if (NumberChanged != null) {    // 觸發事件
            string rtn = NumberChanged();
            Console.WriteLine(rtn);     // 打印返回的字符串,輸出爲Subscriber3
        }
    }
}

// 定義事件訂閱者
public class Subscriber1
    public string OnNumberChanged() {
        return "Subscriber1";
    }
}
public class Subscriber2 { /* 略,與上相似,返回Subscriber2*/ }
public class Subscriber3 { /* 略,與上相似,返回Subscriber3*/ }this

若是運行這段代碼,獲得的輸出是Subscriber3,能夠看到,只獲得了最後一個註冊方法的返回值。

如何讓事件只容許一個客戶訂閱?

少數狀況下,好比像上面,爲了不發生「值覆蓋」的狀況(更可能是在異步調用方法時,後面會討論),咱們可能想限制只容許一個客戶端註冊。此時怎麼作呢?咱們能夠向下面這樣,將事件聲明爲private的,而後提供兩個方法來進行註冊和取消註冊:

// 定義事件發佈者
public class Publishser {
    private event GeneralEventHandler NumberChanged;    // 聲明一個私有事件
    // 註冊事件
    public void Register(GeneralEventHandler method) {
        NumberChanged = method;
    }
    // 取消註冊
    public void UnRegister(GeneralEventHandler method) {
        NumberChanged -= method;
    }

    public void DoSomething() {
        // 作某些其他的事情
        if (NumberChanged != null) {    // 觸發事件
            string rtn = NumberChanged();
            Console.WriteLine("Return: {0}", rtn);      // 打印返回的字符串,輸出爲Subscriber3
        }
    }
}

NOTE:注意上面,在UnRegister()中,沒有進行任何判斷就使用了NumberChanged-=method語句。這是由於即便method方法沒有進行過註冊,此行語句也不會有任何問題,不會拋出異常,僅僅是不會產生任何效果而已。

注意在Register()方法中,咱們使用了賦值操做符「=」,而非「+=」,經過這種方式就避免了多個方法註冊。上面的代碼儘管能夠完成咱們的須要,可是此時你們還應該注意下面兩點:

一、將NumberChanged聲明爲委託變量仍是事件都無所謂了,由於它是私有的,即使將它聲明爲一個委託變量,客戶端也看不到它,也就沒法經過它來觸發事件、調用訂閱者的方法。而只能經過Register()和UnRegister()方法來註冊和取消註冊,經過調用DoSomething()方法觸發事件(而不是NumberChanged自己,這在前面已經討論過了)。

二、咱們還應該發現,這裏採用的、對NumberChanged委託變量的訪問模式和C#中的屬性是多麼相似啊?你們知道,在C#中一般一個屬性對應一個類型成員,而在類型的外部對成員的操做所有經過屬性來完成。儘管這裏對委託變量的處理是相似的效果,但卻使用了兩個方法來進行模擬,有沒有辦法像使用屬性同樣來完成上面的例子呢?答案是有的,C#中提供了一種叫事件訪問器(Event Accessor)的東西,它用來封裝委託變量。以下面例子所示:

class Program {
    static void Main(string[] args) {
        Publishser pub = new Publishser();
        Subscriber1 sub1 = new Subscriber1();
        Subscriber2 sub2 = new Subscriber2();

        pub.NumberChanged -= sub1.OnNumberChanged;  // 不會有任何反應
        pub.NumberChanged += sub2.OnNumberChanged;  // 註冊了sub2
        pub.NumberChanged += sub1.OnNumberChanged;  // sub1將sub2的覆蓋掉了
       
        pub.DoSomething();          // 觸發事件
    }
}

// 定義委託
public delegate string GeneralEventHandler();

// 定義事件發佈者
public class Publishser {
    // 聲明一個委託變量
    private GeneralEventHandler numberChanged;
    // 事件訪問器的定義
    public event GeneralEventHandler NumberChanged {
        add {
            numberChanged = value;
        }
        remove {
            numberChanged -= value;
        }
    }
   
    public void DoSomething() {
        // 作某些其餘的事情
        if (numberChanged != null) {    // 經過委託變量觸發事件
            string rtn = numberChanged();
            Console.WriteLine("Return: {0}", rtn);      // 打印返回的字符串
        }
    }
}

// 定義事件訂閱者
public class Subscriber1 {
    public string OnNumberChanged() {
        Console.WriteLine("Subscriber1 Invoked!");
        return "Subscriber1";
    }
}
public class Subscriber2 {/* 與上類同,略 */}
public class Subscriber3 {/* 與上類同,略 */}

上面代碼中相似屬性的public event GeneralEventHandler NumberChanged {add{...}remove{...}}語句即是事件訪問器。使用了事件訪問器之後,在DoSomething方法中便只能經過numberChanged委託變量來觸發事件,而不能NumberChanged事件訪問器(注意它們的大小寫不一樣)觸發,它只用於註冊和取消註冊。下面是代碼輸出:

Subscriber1 Invoked!
Return: Subscriber1

得到多個返回值與異常處理

如今假設咱們想要得到多個訂閱者的返回值,以List<string>的形式返回,該如何作呢?咱們應該記得委託定義在編譯時會生成一個繼承自MulticastDelegate的類,而這個MulticastDelegate又繼承自Delegate,在Delegate內部,維護了一個委託鏈表,鏈表上的每個元素,爲一個只包含一個目標方法的委託對象。而經過Delegate基類的GetInvocationList()靜態方法,能夠得到這個委託鏈表。隨後咱們遍歷這個鏈表,經過鏈表中的每一個委託對象來調用方法,這樣就能夠分別得到每一個方法的返回值:

class Program4 {
    static void Main(string[] args) {
        Publishser pub = new Publishser();
        Subscriber1 sub1 = new Subscriber1();
        Subscriber2 sub2 = new Subscriber2();
        Subscriber3 sub3 = new Subscriber3();

        pub.NumberChanged += new DemoEventHandler(sub1.OnNumberChanged);
        pub.NumberChanged += new DemoEventHandler(sub2.OnNumberChanged);
        pub.NumberChanged += new DemoEventHandler(sub3.OnNumberChanged);

        List<string> list = pub.DoSomething();  //調用方法,在方法內觸發事件

        foreach (string str in list) {
            Console.WriteLine(str);
        }          
    }
}

public delegate string DemoEventHandler(int num);

// 定義事件發佈者
public class Publishser {
    public event DemoEventHandler NumberChanged;    // 聲明一個事件

    public List<string> DoSomething() {
        // 作某些其餘的事

        List<string> strList = new List<string>();
        if (NumberChanged == null) return strList;

        // 得到委託數組
        Delegate[] delArray = NumberChanged.GetInvocationList();

        foreach (Delegate del in delArray) {
            // 進行一個向下轉換
            DemoEventHandler method = (DemoEventHandler)del;
            strList.Add(method(100));       // 調用方法並獲取返回值
        }
       
        return strList;
    }
}

// 定義事件訂閱者
public class Subscriber1 {
    public string OnNumberChanged(int num) {
        Console.WriteLine("Subscriber1 invoked, number:{0}", num);
        return "[Subscriber1 returned]";
    }
}
public class Subscriber3 {與上面類同,略}
public class Subscriber3 {與上面類同,略}

若是運行上面的代碼,能夠獲得這樣的輸出:

Subscriber1 invoked, number:100
Subscriber2 invoked, number:100
Subscriber3 invoked, number:100
[Subscriber1 returned]
[Subscriber2 returned]
[Subscriber3 returned]

可見咱們得到了三個方法的返回值。而咱們前面說過,不少狀況下委託的定義都不包含返回值,因此上面介紹的方法彷佛沒有什麼實際意義。其實經過這種方式來觸發事件最多見的狀況應該是在異常處理中,由於頗有可能在觸發事件時,訂閱者的方法會拋出異常,而這一異常會直接影響到發佈者,使得發佈者程序停止,然後面訂閱者的方法將不會被執行。所以咱們須要加上異常處理,考慮下面一段程序:

class Program5 {
    static void Main(string[] args) {
        Publisher pub = new Publisher();
        Subscriber1 sub1 = new Subscriber1();
        Subscriber2 sub2 = new Subscriber2();
        Subscriber3 sub3 = new Subscriber3();

        pub.NumberChanged += new DemoEventHandler(sub1.OnNumberChanged);
        pub.NumberChanged += new DemoEventHandler(sub2.OnNumberChanged);
        pub.NumberChanged += new DemoEventHandler(sub3.OnNumberChanged);
    }
}

public class Publisher {
    public event EventHandler MyEvent;
    public void DoSomething() {
        // 作某些其餘的事情
        if (MyEvent != null) {
            try {
                MyEvent(this, EventArgs.Empty);
            } catch (Exception e) {
                Console.WriteLine("Exception: {0}", e.Message);
            }
        }
    }
}

public class Subscriber1 {
    public void OnEvent(object sender, EventArgs e) {
        Console.WriteLine("Subscriber1 Invoked!");
    }
}

public class Subscriber2 {
    public void OnEvent(object sender, EventArgs e) {
        throw new Exception("Subscriber2 Failed");
    }
}
public class Subscriber3 {/* 與Subsciber1類同,略*/}

注意到咱們在Subscriber2中拋出了異常,同時咱們在Publisher中使用了try/catch語句來處理異常。運行上面的代碼,咱們獲得的結果是:

Subscriber1 Invoked!
Exception: Subscriber2 Failed

能夠看到,儘管咱們捕獲了異常,使得程序沒有異常結束,可是卻影響到了後面的訂閱者,由於Subscriber3也訂閱了事件,可是卻沒有收到事件通知(它的方法沒有被調用)。此時,咱們能夠採用上面的辦法,先得到委託鏈表,而後在遍歷鏈表的循環中處理異常,咱們只須要修改一下DoSomething方法就能夠了:

public void DoSomething() {
    if (MyEvent != null) {
        Delegate[] delArray = MyEvent.GetInvocationList();
        foreach (Delegate del in delArray) {
            EventHandler method = (EventHandler)del;    // 強制轉換爲具體的委託類型
            try {
                method(this, EventArgs.Empty);
            } catch (Exception e) {
                Console.WriteLine("Exception: {0}", e.Message);
            }
        }
    }
}

注意到Delegate是EventHandler的基類,因此爲了觸發事件,先要進行一個向下的強制轉換,以後才能在其上觸發事件,調用全部註冊對象的方法。除了使用這種方式之外,還有一種更靈活方式能夠調用方法,它是定義在Delegate基類中的DynamicInvoke()方法:

public object DynamicInvoke(params object[] args);

這多是調用委託最通用的方法了,適用於全部類型的委託。它接受的參數爲object[],也就是說它能夠將任意數量的任意類型做爲參數,並返回單個object對象。上面的DoSomething()方法也能夠改寫成下面這種通用形式:

public void DoSomething() {
    // 作某些其餘的事情
    if (MyEvent != null) {
        Delegate[] delArray = MyEvent.GetInvocationList();
        foreach (Delegate del in delArray) {                   
            try {
                // 使用DynamicInvoke方法觸發事件
                del.DynamicInvoke(this, EventArgs.Empty);  
            } catch (Exception e) {
                Console.WriteLine("Exception: {0}", e.Message);
            }
        }
    }
}

注意如今在DoSomething()方法中,咱們取消了向具體委託類型的向下轉換,如今沒有了任何的基於特定委託類型的代碼,而DynamicInvoke又能夠接受任何類型的參數,且返回一個object對象。因此咱們徹底能夠將DoSomething()方法抽象出來,使它成爲一個公共方法,而後供其餘類來調用,咱們將這個方法聲明爲靜態的,而後定義在Program類中:

// 觸發某個事件,以列表形式返回全部方法的返回值
public static object[] FireEvent(Delegate del, params object[] args){

    List<object> objList = new List<object>();

    if (del != null) {
        Delegate[] delArray = del.GetInvocationList();
        foreach (Delegate method in delArray) {
            try {
                // 使用DynamicInvoke方法觸發事件
                object obj = method.DynamicInvoke(args);
                if (obj != null)
                    objList.Add(obj);
            } catch { }
        }
    }
    return objList.ToArray();
}

隨後,咱們在DoSomething()中只要簡單的調用一下這個方法就能夠了:

public void DoSomething() {
    // 作某些其餘的事情
    Program5.FireEvent(MyEvent, this, EventArgs.Empty);
}

注意FireEvent()方法還能夠返回一個object[]數組,這個數組包括了全部訂閱者方法的返回值。而在上面的例子中,我沒有演示如何獲取並使用這個數組,爲了節省篇幅,這裏也再也不贅述了,在本文附帶的代碼中,有關於這部分的演示,有興趣的朋友能夠下載下來看看。

委託中訂閱者方法超時的處理

訂閱者除了能夠經過異常的方式來影響發佈者之外,還能夠經過另外一種方式:超時。通常說超時,指的是方法的執行超過某個指定的時間,而這裏我將含義擴展了一下,凡是方法執行的時間比較長,我就認爲它超時了,這個「比較長」是一個比較模糊的概念,2秒、3秒、5秒均可以視爲超時。超時和異常的區別就是超時並不會影響事件的正確觸發和程序的正常運行,卻會致使事件觸發後須要很長才可以結束。在依次執行訂閱者的方法這段期間內,客戶端程序會被中斷,什麼也不能作。由於當執行訂閱者方法時(經過委託,至關於依次調用全部註冊了的方法),當前線程會轉去執行方法中的代碼,調用方法的客戶端會被中斷,只有當方法執行完畢並返回時,控制權纔會回到客戶端,從而繼續執行下面的代碼。咱們來看一下下面一個例子:

class Program6 {
    static void Main(string[] args) {

        Publisher pub = new Publisher();
        Subscriber1 sub1 = new Subscriber1();
        Subscriber2 sub2 = new Subscriber2();
        Subscriber3 sub3 = new Subscriber3();

        pub.MyEvent += new EventHandler(sub1.OnEvent);
        pub.MyEvent += new EventHandler(sub2.OnEvent);
        pub.MyEvent += new EventHandler(sub3.OnEvent);

        pub.DoSomething();      // 觸發事件

        Console.WriteLine(" Control back to client!"); // 返回控制權
    }

    // 觸發某個事件,以列表形式返回全部方法的返回值
    public static object[] FireEvent(Delegate del, params object[] args) {
        // 代碼與上同,略
    }
}

public class Publisher {
    public event EventHandler MyEvent;
    public void DoSomething() {
        // 作某些其餘的事情
        Console.WriteLine("DoSomething invoked!");
        Program6.FireEvent(MyEvent, this, EventArgs.Empty); //觸發事件
    }
}

public class Subscriber1 {
    public void OnEvent(object sender, EventArgs e) {
        Thread.Sleep(TimeSpan.FromSeconds(3));
        Console.WriteLine("Waited for 3 seconds, subscriber1 invoked!");
    }
}
public class Subscriber2 {
    public void OnEvent(object sender, EventArgs e) {
        Console.WriteLine("Subscriber2 immediately Invoked!");
    }
}
public class Subscriber3 {
    public void OnEvent(object sender, EventArgs e) {
        Thread.Sleep(TimeSpan.FromSeconds(2));
        Console.WriteLine("Waited for 2 seconds, subscriber2 invoked!");
    }
}

在這段代碼中,咱們使用Thread.Sleep()靜態方法模擬了方法超時的狀況。其中Subscriber1.OnEvent()須要三秒鐘完成,Subscriber2.OnEvent()當即執行,Subscriber3.OnEvent須要兩秒完成。這段代碼徹底能夠正常輸出,也沒有異常拋出(若是有,也僅僅是該訂閱者被忽略掉),下面是輸出的狀況:

DoSomething invoked!
Waited for 3 seconds, subscriber1 invoked!
Subscriber2 immediately Invoked!
Waited for 2 seconds, subscriber2 invoked!

Control back to client!

可是這段程序在調用方法DoSomething()、打印了「DoSomething invoked」以後,觸發了事件,隨後必須等訂閱者的三個方法所有執行完畢了以後,也就是大概5秒鐘的時間,才能繼續執行下面的語句,也就是打印「Control back to client」。而咱們前面說過,不少狀況下,尤爲是遠程調用的時候(好比說在Remoting中),發佈者和訂閱者應該是徹底的鬆耦合,發佈者不關心誰訂閱了它、不關心訂閱者的方法有什麼返回值、不關心訂閱者會不會拋出異常,固然也不關心訂閱者須要多長時間才能完成訂閱的方法,它只要在事件發生的那一瞬間告知訂閱者事件已經發生並將相關參數傳給訂閱者就能夠了。而後它就應該繼續執行它後面的動做,在本例中就是打印「Control back to client!」。而訂閱者無論失敗或是超時都不該該影響到發佈者,但在上面的例子中,發佈者卻不得不等待訂閱者的方法執行完畢才能繼續運行。

如今咱們來看下如何解決這個問題,先回顧一下以前我在C#中的委託和事件一文中提到的內容,我說過,委託的定義會生成繼承自MulticastDelegate的完整的類,其中包含Invoke()、BeginInvoke()和EndInvoke()方法。當咱們直接調用委託時,其實是調用了Invoke()方法,它會中斷調用它的客戶端,而後在客戶端線程上執行全部訂閱者的方法(客戶端沒法繼續執行後面代碼),最後將控制權返回客戶端。注意到BeginInvoke()、EndInvoke()方法,在.Net中,異步執行的方法一般都會配對出現,而且以Begin和End做爲方法的開頭(最多見的可能就是Stream類的BeginRead()和EndRead()方法了)。它們用於方法的異步執行,便是在調用BeginInvoke()以後,客戶端從線程池中抓取一個閒置線程,而後交由這個線程去執行訂閱者的方法,而客戶端線程則能夠繼續執行下面的代碼。

BeginInvoke()接受「動態」的參數個數和類型,爲何說「動態」的呢?由於它的參數是在編譯時根據委託的定義動態生成的,其中前面參數的個數和類型與委託定義中接受的參數個數和類型相同,最後兩個參數分別是AsyncCallback和Object類型,對於它們更具體的內容,能夠參見下一節委託和方法的異步調用部分。如今,咱們僅須要對這兩個參數傳入null就能夠了。另外還須要注意幾點:

  • 在委託類型上調用BeginInvoke()時,此委託對象只能包含一個目標方法,因此對於多個訂閱者註冊的狀況,必須使用GetInvocationList()得到全部委託對象,而後遍歷它們,分別在其上調用BeginInvoke()方法。若是直接在委託上調用BeginInvoke(),會拋出異常,提示「委託只能包含一個目標方法」。
  • 若是訂閱者的方法拋出異常,.NET會捕捉到它,可是隻有在調用EndInvoke()的時候,纔會將異常從新拋出。而在本例中,咱們不使用EndInvoke()(由於咱們不關心訂閱者的執行狀況),因此咱們無需處理異常,由於即便拋出異常,也是在另外一個線程上,不會影響到客戶端線程(客戶端甚至不知道訂閱者發生了異常,這有時是好事有時是壞事)。
  • BeginInvoke()方法屬於委託定義所生成的類,它既不屬於MulticastDelegate也不屬於Delegate基類,因此沒法繼續使用可重用的FireEvent()方法,咱們須要進行一個向下轉換,來獲取到實際的委託類型。

如今咱們修改一下上面的程序,使用異步調用來解決訂閱者方法執行超時的狀況:

class Program6 {
    static void Main(string[] args) {

        Publisher pub = new Publisher();
        Subscriber1 sub1 = new Subscriber1();
        Subscriber2 sub2 = new Subscriber2();
        Subscriber3 sub3 = new Subscriber3();

        pub.MyEvent += new EventHandler(sub1.OnEvent);
        pub.MyEvent += new EventHandler(sub2.OnEvent);
        pub.MyEvent += new EventHandler(sub3.OnEvent);

        pub.DoSomething();      // 觸發事件

        Console.WriteLine("Control back to client! "); // 返回控制權
        Console.WriteLine("Press any thing to exit...");
        Console.ReadKey();      // 暫停客戶程序,提供時間供訂閱者完成方法
    }
}

public class Publisher {
    public event EventHandler MyEvent;
    public void DoSomething() {        
        // 作某些其餘的事情
        Console.WriteLine("DoSomething invoked!");

        if (MyEvent != null) {
            Delegate[] delArray = MyEvent.GetInvocationList();

            foreach (Delegate del in delArray) {
                EventHandler method = (EventHandler)del;
                method.BeginInvoke(null, EventArgs.Empty, null, null);
            }
        }
    }
}

public class Subscriber1 {
    public void OnEvent(object sender, EventArgs e) {
        Thread.Sleep(TimeSpan.FromSeconds(3));      // 模擬耗時三秒才能完成方法
        Console.WriteLine("Waited for 3 seconds, subscriber1 invoked!");
    }
}

public class Subscriber2 {
    public void OnEvent(object sender, EventArgs e) {
        throw new Exception("Subsciber2 Failed");   // 即便拋出異常也不會影響到客戶端
        //Console.WriteLine("Subscriber2 immediately Invoked!");
    }
}

public class Subscriber3 {
    public void OnEvent(object sender, EventArgs e) {
        Thread.Sleep(TimeSpan.FromSeconds(2));  // 模擬耗時兩秒才能完成方法
        Console.WriteLine("Waited for 2 seconds, subscriber3 invoked!");
    }
}

運行上面的代碼,會獲得下面的輸出:

DoSomething invoked!
Control back to client!

Press any thing to exit...

Waited for 2 seconds, subscriber3 invoked!
Waited for 3 seconds, subscriber1 invoked!

須要注意代碼輸出中的幾個變化:

  1. 咱們須要在客戶端程序中調用Console.ReadKey()方法來暫停客戶端,以提供足夠的時間來讓異步方法去執行完代碼,否則的話客戶端的程序到此處便會運行結束,程序會退出,不會看到任何訂閱者方法的輸出,由於它們根本沒來得及執行完畢。緣由是這樣的:客戶端所在的線程咱們一般稱爲主線程,而執行訂閱者方法的線程來自線程池,屬於後臺線程(Background Thread),當主線程結束時,不論後臺線程有沒有結束,都會退出程序。(固然還有一種前臺線程(Foreground Thread),主線程結束後必須等前臺線程也結束後程序纔會退出,關於線程的討論能夠開闢另外一個龐大的主題,這裏就不討論了)。
  2. 在打印完「Press any thing to exit...」以後,兩個訂閱者的方法會以2秒、1秒的間隔顯示出來,且儘管咱們先註冊了subscirber1,可是卻先執行了subscriber3,這是由於執行它須要的時間更短。除此之外,注意到這兩個方法是並行執行的,因此執行它們的總時間是最長的方法所須要的時間,也就是3秒,而不是他們的累加5秒。
  3. 如同前面所提到的,儘管subscriber2拋出了異常,咱們也沒有針對異常進行處理,可是客戶程序並無察覺到,程序也沒有所以而中斷。

委託和方法的異步調用

一般狀況下,若是須要異步執行一個耗時的操做,咱們會新起一個線程,而後讓這個線程去執行代碼。可是對於每個異步調用都經過建立線程來進行操做顯然會對性能產生必定的影響,同時操做也相對繁瑣一些。.Net中能夠經過委託進行方法的異步調用,就是說客戶端在異步調用方法時,自己並不會由於方法的調用而中斷,而是從線程池中抓取一個線程去執行該方法,自身線程(主線程)在完成抓取線程這一過程以後,繼續執行下面的代碼,這樣就實現了代碼的並行執行。使用線程池的好處就是避免了頻繁進行異步調用時建立、銷燬線程的開銷。

如同上面所示,當咱們在委託對象上調用BeginInvoke()時,便進行了一個異步的方法調用。上面的例子中是在事件的發佈和訂閱這一過程當中使用了異步調用,而在事件發佈者和訂閱者之間每每是鬆耦合的,發佈者一般不須要得到訂閱者方法執行的狀況;而當使用異步調用時,更多狀況下是爲了提高系統的性能,而並不是專用於事件的發佈和訂閱這一編程模型。而在這種狀況下使用異步編程時,就須要進行更多的控制,好比當異步執行方法的方法結束時通知客戶端、返回異步執行方法的返回值等。本節就對BeginInvoke()方法、EndInvoke()方法和其相關的IAysncResult作一個簡單的介紹。

NOTE:注意此處我已經再也不使用發佈者、訂閱者這些術語,由於咱們再也不是討論上面的事件模型,而是討論在客戶端程序中異步地調用方法,這裏有一個思惟的轉變。

咱們看這樣一段代碼,它演示了不使用異步調用的一般狀況:

class Program7 {
    static void Main(string[] args) {

        Console.WriteLine("Client application started! ");
        Thread.CurrentThread.Name = "Main Thread";

        Calculator cal = new Calculator();
        int result = cal.Add(2, 5);
        Console.WriteLine("Result: {0} ", result);
       
        // 作某些其它的事情,模擬須要執行3秒鐘
        for (int i = 1; i <= 3; i++) {
            Thread.Sleep(TimeSpan.FromSeconds(i));
            Console.WriteLine("{0}: Client executed {1} second(s).",
                Thread.CurrentThread.Name, i); 
        }

        Console.WriteLine(" Press any key to exit...");
        Console.ReadKey();
    }
}

public class Calculator {
    public int Add(int x, int y) {
        if (Thread.CurrentThread.IsThreadPoolThread) {
            Thread.CurrentThread.Name = "Pool Thread";
        }
        Console.WriteLine("Method invoked!");          

        // 執行某些事情,模擬須要執行2秒鐘
        for (int i = 1; i <= 2; i++) {
            Thread.Sleep(TimeSpan.FromSeconds(i));
            Console.WriteLine("{0}: Add executed {1} second(s).",
                Thread.CurrentThread.Name, i); 
        }
        Console.WriteLine("Method complete!");
        return x + y;
    }
}

上面代碼有幾個關於對於線程的操做,若是不瞭解能夠看一下下面的說明,若是你已經瞭解能夠直接跳過:

  • Thread.Sleep(),它會讓執行當前代碼的線程暫停一段時間(若是你對線程的概念比較陌生,能夠理解爲使程序的執行暫停一段時間),以毫秒爲單位,好比Thread.Sleep(1000),將會使線程暫停1秒鐘。在上面我使用了它的重載方法,我的以爲使用TimeSpan.FromSeconds(1),可讀性更好一些。
  • Thread.CurrentThread.Name,經過這個屬性能夠設置、獲取執行當前代碼的線程的名稱,值得注意的是這個屬性只能夠設置一次,若是設置兩次,會拋出異常。
  • Thread.IsThreadPoolThread,能夠判斷執行當前代碼的線程是否爲線程池中的線程。

經過這幾個方法和屬性,有助於咱們更好地調試異步調用方法。上面代碼中除了加入了一些對線程的操做之外再沒有什麼特別之處。咱們建了一個Calculator類,它只有一個Add方法,咱們模擬了這個方法須要執行2秒鐘時間,而且每隔一秒進行一次輸出。而在客戶端程序中,咱們使用result變量保存了方法的返回值並進行了打印。隨後,咱們再次模擬了客戶端程序接下來的操做須要執行2秒鐘時間。運行這段程序,會產生下面的輸出:

Client application started!

Method invoked!
Main Thread: Add executed 1 second(s).
Main Thread: Add executed 2 second(s).
Method complete!
Result: 7

Main Thread: Client executed 1 second(s).
Main Thread: Client executed 2 second(s).
Main Thread: Client executed 3 second(s).

Press any key to exit...

若是你確實執行了這段代碼,會看到這些輸出並非一瞬間輸出的,而是執行了大概5秒鐘的時間,由於線程是串行執行的,因此在執行完Add()方法以後纔會繼續客戶端剩下的代碼。

接下來咱們定義一個AddDelegate委託,並使用BeginInvoke()方法來異步地調用它。在上面已經介紹過,BeginInvoke()除了最後兩個參數爲AsyncCallback類型和Object類型之外,前面的參數類型和個數與委託定義相同。另外BeginInvoke()方法返回了一個實現了IAsyncResult接口的對象(實際上就是一個AsyncResult類型實例,注意這裏IAsyncResult和AysncResult是不一樣的,它們均包含在.Net Framework中)。

AsyncResult的用途有這麼幾個:傳遞參數,它包含了對調用了BeginInvoke()的委託的引用;它還包含了BeginInvoke()的最後一個Object類型的參數;它能夠鑑別出是哪一個方法的哪一次調用,由於經過同一個委託變量能夠對同一個方法調用屢次。

EndInvoke()方法接受IAsyncResult類型的對象(以及ref和out類型參數,這裏不討論了,對它們的處理和返回值相似),因此在調用BeginInvoke()以後,咱們須要保留IAsyncResult,以便在調用EndInvoke()時進行傳遞。這裏最重要的就是EndInvoke()方法的返回值,它就是方法的返回值。除此之外,當客戶端調用EndInvoke()時,若是異步調用的方法沒有執行完畢,則會中斷當前線程而去等待該方法,只有當異步方法執行完畢後纔會繼續執行後面的代碼。因此在調用完BeginInvoke()後當即執行EndInvoke()是沒有任何意義的。咱們一般在儘量早的時候調用BeginInvoke(),而後在須要方法的返回值的時候再去調用EndInvoke(),或者是根據狀況在晚些時候調用。說了這麼多,咱們如今看一下使用異步調用改寫後上面的代碼吧:

public delegate int AddDelegate(int x, int y);

class Program8 {   

    static void Main(string[] args) {

        Console.WriteLine("Client application started! ");
        Thread.CurrentThread.Name = "Main Thread";
                   
        Calculator cal = new Calculator();
        AddDelegate del = new AddDelegate(cal.Add);
        IAsyncResult asyncResult = del.BeginInvoke(2,5,null,null);  // 異步調用方法

        // 作某些其它的事情,模擬須要執行3秒鐘
        for (int i = 1; i <= 3; i++) {
            Thread.Sleep(TimeSpan.FromSeconds(i));
            Console.WriteLine("{0}: Client executed {1} second(s).",
                Thread.CurrentThread.Name, i);
        }

        int rtn = del.EndInvoke(asyncResult);
        Console.WriteLine("Result: {0} ", rtn);

        Console.WriteLine(" Press any key to exit...");
        Console.ReadKey();
    }
}

public class Calculator { /* 與上面同,略 */}

此時的輸出爲:

Client application started!

Method invoked!
Main Thread: Client executed 1 second(s).
Pool Thread: Add executed 1 second(s).
Main Thread: Client executed 2 second(s).
Pool Thread: Add executed 2 second(s).
Method complete!
Main Thread: Client executed 3 second(s).
Result: 7


Press any key to exit...

如今執行完這段代碼只須要3秒鐘時間,兩個for循環所產生的輸出交替進行,這也說明了這兩段代碼並行執行的狀況。能夠看到Add()方法是由線程池中的線程在執行,由於Thread.CurrentThread.IsThreadPoolThread返回了True,同時咱們對該線程命名爲了Pool Thread。另外咱們能夠看到經過EndInvoke()方法獲得了返回值。

有時候,咱們可能會將得到返回值的操做放到另外一段代碼或者客戶端去執行,而不是向上面那樣直接寫在BeginInvoke()的後面。好比說咱們在Program中新建一個方法GetReturn(),此時能夠經過AsyncResult的AsyncDelegate得到del委託對象,而後再在其上調用EndInvoke()方法,這也說明了AsyncResult能夠惟一的獲取到與它相關的調用了的方法(或者也能夠理解成委託對象)。因此上面獲取返回值的代碼也能夠改寫成這樣:

static int GetReturn(IAsyncResult asyncResult) {
    AsyncResult result = (AsyncResult)asyncResult;
    AddDelegate del = (AddDelegate)result.AsyncDelegate;
    int rtn = del.EndInvoke(asyncResult);
    return rtn;
}

而後再將int rtn = del.EndInvoke(asyncResult);語句改成int rtn = GetReturn(asyncResult);。注意上面IAsyncResult要轉換爲實際的類型AsyncResult才能訪問AsyncDelegate屬性,由於它沒有包含在IAsyncResult接口的定義中。

BeginInvoke的另外兩個參數分別是AsyncCallback和Object類型,其中AsyncCallback是一個委託類型,它用於方法的回調,便是說當異步方法執行完畢時自動進行調用的方法。它的定義爲:

public delegate void AsyncCallback(IAsyncResult ar);

Object類型用於傳遞任何你想要的數值,它能夠經過IAsyncResult的AsyncState屬性得到。下面咱們將獲取方法返回值、打印返回值的操做放到了OnAddComplete()回調方法中:

public delegate int AddDelegate(int x, int y);

class Program9 {

    static void Main(string[] args) {

        Console.WriteLine("Client application started! ");
        Thread.CurrentThread.Name = "Main Thread";

        Calculator cal = new Calculator();
        AddDelegate del = new AddDelegate(cal.Add);
        string data = "Any data you want to pass.";
        AsyncCallback callBack = new AsyncCallback(OnAddComplete);
        del.BeginInvoke(2, 5, callBack, data);      // 異步調用方法

        // 作某些其它的事情,模擬須要執行3秒鐘
        for (int i = 1; i <= 3; i++) {
            Thread.Sleep(TimeSpan.FromSeconds(i));
            Console.WriteLine("{0}: Client executed {1} second(s).",
                Thread.CurrentThread.Name, i);
        }

        Console.WriteLine(" Press any key to exit...");
        Console.ReadKey();
    }

    static void OnAddComplete(IAsyncResult asyncResult) {
        AsyncResult result = (AsyncResult)asyncResult;
        AddDelegate del = (AddDelegate)result.AsyncDelegate;
        string data = (string)asyncResult.AsyncState;

        int rtn = del.EndInvoke(asyncResult);
        Console.WriteLine("{0}: Result, {1}; Data: {2} ",
            Thread.CurrentThread.Name, rtn, data);
    }
}
public class Calculator { /* 與上面同,略 */}

它產生的輸出爲:

Client application started!

Method invoked!
Main Thread: Client executed 1 second(s).
Pool Thread: Add executed 1 second(s).
Main Thread: Client executed 2 second(s).
Pool Thread: Add executed 2 second(s).
Method complete!
Pool Thread: Result, 7; Data: Any data you want to pass.

Main Thread: Client executed 3 second(s).

Press any key to exit...

這裏有幾個值得注意的地方:一、咱們在調用BeginInvoke()後再也不須要保存IAysncResult了,由於AysncCallback委託將該對象定義在了回調方法的參數列表中;二、咱們在OnAddComplete()方法中得到了調用BeginInvoke()時最後一個參數傳遞的值,字符串「Any data you want to pass」;三、執行回調方法的線程並不是客戶端線程Main Thread,而是來自線程池中的線程Pool Thread。另外如前面所說,在調用EndInvoke()時有可能會拋出異常,因此在應該將它放到try/catch塊中,這裏我就再也不示範了。

總結

這篇文章是對我以前寫的C#中的委託和事件的一個補充,大體分爲了三個部分,第一部分講述了幾個容易讓人產生困惑的問題:爲何使用事件而不是委託變量,爲何一般委託的定義都返回void;第二部分講述瞭如何處理異常和超時;第三部分則講述了經過委託實現異步方法的調用。

感謝閱讀,但願這篇文章能給你帶來幫助。

源碼下載:http://www.tracefact.net/SourceCode/MoreDelegate.rar

相關文章
相關標籤/搜索