C#事件與委託詳解【精華 多看看】

原文鏈接:https://blog.csdn.net/jamestaosh/article/details/4372172#commentBox

Delegate
delegate是C#中的一種類型,它
實際上是一個能夠持有對某個方法的引用的類與其它的類不同,delegate類能夠擁有一個簽名(signature),並且它"只能持有與它的簽名相匹配的方法的引用"。它所實現的功能與C/C++中的函數指針十分相似。它允許你傳遞一個類A的方法m給另一個類B的對象,使得類B的對象能夠調用這個方法m。但與函數指針相比,delegate有許多函數委託和事件在 .Net Framework中的應用非常廣泛指針不具備的優點。首先,函數指針只能指向靜態函數,而delegate既可以引用靜態函數,又可以引用非靜態成員函數。在引用非靜態成員函數時,delegate不但保存了對此函數入口指針的引用,而且還保存了調用此函數的類實例的引用。其次,與函數指針相比,delegate是面向對象、類型安全、可靠的受控(managed)對象。也就是說,runtime能夠保證delegate指向一個有效的方法,你無須擔心delegate會指向無效地址或者越界地址


實現一個delegate是很簡單的,通過以下3個步驟即可實現一個delegate:
1. 聲明一個delegate對象,它應當與你想要傳遞的方法具有相同的參數和返回值類型。
2. 創建delegate對象,並"將你想要傳遞的函數作爲參數傳入"
3. 在要實現異步調用的地方,通過上一步創建的對象來調用方法

using System;


public class MyDelegateTest
{
        // 步驟1,
聲明delegate對象
        public delegate void MyDelegate(string name);

        // 這是我們
欲傳遞的方法,它與MyDelegate具有相同的參數和返回值類型
        public static void MyDelegateFunc(string name)
        {
                  Console.WriteLine("Hello, ", name);
        }
        public static void Main()
        {
                  // 步驟2,
創建delegate對象(實例??)
 MyDelegate md = new MyDelegate(MyDelegateTest.MyDelegateFunc);
                 // 步驟3,
調用delegate
                 md("sam1111");
        }
}
輸出結果是:Hello, sam1111



瞭解了delegate,下面我們來看看,在C#中對事件是如何處理的。

C#中的事件處理實際上是一種具有特殊簽名的delegate,象下面這個樣子:
public delegate void MyEventHandler(object sender, MyEventArgs e);

其中的兩個參數,sender代表事件發送者,e是事件參數類。
MyEventArgs類用來包含與事件相關的數據,所有的事件參數類都必須從System.EventArgs類派生。當然,如果你的事件不含參數,那麼可以直接用System.EventArgs類作爲參數。

就是這麼簡單,結合delegate的實現,
我們可以將自定義事件的實現歸結爲以下幾步:
1.定義delegate對象類型,它有兩個參數,第一個參數是事件發送者對象,第二個參數是事件參數類對象。
2.定義事件參數類,此類應當從System.EventArgs類派生。如果事件不帶參數,這一步可以省略。
3.定義"事件處理方法,它應當與delegate對象具有相同的參數和返回值類型"
4.用event關鍵字定義事件對象,它同時也是一個delegate對象
5.用+=操作符添加事件到事件隊列中(-=操作符能夠將事件從隊列中刪除)。
6.在需要觸發事件的地方
用調用delegate的方式寫事件觸發方法一般來說,此方法應爲protected訪問限制,既不能以public方式調用,但可以被子類繼承。名字是OnEventName。
7.
在適當的地方調用事件觸發方法觸發事件


下面是一個簡單的例子:

using System;
public class EventTest
{
        // 步驟1,
定義delegate對象
       public delegate void MyEventHandler(object sender, System.EventArgs e);
       // 步驟2(定義事件參數類)省略
       public class MyEventCls
       {
                // 步驟3,
定義事件處理方法,它與delegate對象具有相同的參數和返回值類
                public  void MyEventFunc(object sender, System.EventArgs e)
                {
                           Console.WriteLine("My event is ok!");
                }
       }
       // 步驟4,
用event關鍵字定義事件對象
       private event MyEventHandler myevent;
       private MyEventCls myecls;

      public EventTest()
      {
                myecls = new MyEventCls();
          
// 步驟5,用+=操作符將事件添加到隊列中
                this.myevent += new MyEventHandler(myecls.MyEventFunc);
      }
      // 步驟6,
以調用delegate的方式事件觸發函數
     protected void OnMyEvent(System.EventArgs e)
      {
               if(myevent != null)
                       myevent(this, e);
      }
     public void RaiseEvent()
      {
               EventArgs e = new EventArgs();
      
       // 步驟7,觸發事件
               OnMyEvent(e);
      }
      public static void Main()
      {
               EventTest et = new EventTest();
               Console.Write("Please input ''a'':");
               string s = Console.ReadLine();
               if(s == "a")
               {
                     et.RaiseEvent();
               }
               else
              {
                        Console.WriteLine("Error");
              }
      }
}
輸出結果如下,紅色爲用戶的輸入:
Please input ‘a’: 
a
My event is ok!

 

 

-----------------------------------------------

 

[叩響C#之門]寫給初學者:自定義事件

要創建一個事件驅動的程序需要下面的步驟:

1.         聲明關於事件的委託;

2.         聲明事件;

3.         編寫觸發事件的函數;

4.         創建事件處理程序;

5.         註冊事件處理程序;

6.         在適當的條件下觸發事件。 

現在我們來編寫一個自定義事件的程序。主人養了一條忠實的看門狗,晚上主人睡覺的時候,狗負責看守房子。一旦有小偷進來,狗就發出一個Alarm事件,主人接到Alarm事件後就會採取相應的行動。假設小偷於2009年元旦午夜時分到達。

 

 

//事件發送者

class Dog
{
    //1.聲明關於事件的委託;
    public delegate void AlarmEventHandler(object sender, EventArgs e);
//2.<strong>聲明事件</strong>;   
public event <strong>Alarm<span style="text-decoration:underline;">EventHandler</span></strong> Alarm;

//3.<strong>編寫引發事件的函數</strong>;
public void <strong><span style="text-decoration:underline;">On</span>Alarm</strong>()
{
    if (this.Alarm != null)
    {
        Console.WriteLine("/n狗報警: 有小偷進來了,汪汪~~~~~~~");
        this.Alarm(this, new EventArgs());   //發出警報
     }
}

}

//事件接收者
class Host
{
//4.編寫事件處理程序
void HostHandleAlarm(object sender, EventArgs e)
{
Console.WriteLine(“主 人: 抓住了小偷!”);
}

//5.<strong>註冊事件處理程序</strong>
public Host(Dog dog)
{
    dog.Alarm += new <strong><span style="text-decoration:underline;">Dog.AlarmEventHandler</span>(<span style="text-decoration:underline;">HostHandleAlarm</span>)</strong>;
}

}

//6.現在來觸發事件
class Program
{
static void Main(string[] args)
{
Dog dog = new Dog();
Host host = new Host(dog);

    //當前時間,從2008年12月31日23:59:50開始計時
    DateTime now = new DateTime(2008, 12, 31, 23, 59, 50);
    DateTime midnight = new DateTime(2009, 1, 1, 0, 0, 0);

    //等待午夜的到來
    Console.WriteLine("時間一秒一秒地流逝... ");
    while (now &lt; midnight)
    {
        Console.WriteLine("當前時間: " + now);
        System.Threading.Thread.Sleep(1000);   //程序暫停一秒
        now = now.AddSeconds(1);                //時間增加一秒
     }

    //午夜零點小偷到達,看門狗引發Alarm事件
    Console.WriteLine("/n月黑風高的午夜: " + now);
    Console.WriteLine("小偷悄悄地摸進了主人的屋內... ");
    dog.OnAlarm();
}

}  

 

 

 

當午夜時分小偷到達時,dog調用dog.OnAlarm()函數,從而觸發Alarm事件,於是"系統"找到並執行了註冊在Alarm事件中的事件處理程序HostHandleAlarm()

事件處理委託習慣上EventHandler結尾,比如AlarmEventHandler事件Alarm實際上是事件處理委託AlarmEventHandler的一個實例引發事件的代碼常常被編寫成一個函數,.NET約定這種函數的名稱爲“OnEventName,比如OnAlarm()的函數。在Host類中,我們定義了事件處理程序HostHandleAlarm(),並把它註冊到dog.Alarm事件中。 

 

       事件處理程序的參數應該和事件委託相同。一般情況下,事件處理程序接受兩個參數,一個是事件的發送者sender,一個是事件參數e。事件參數用於在發送者和接收者之間傳遞信息。

 

 

.NET提供了100個事件參數類,這些都繼承於EventArgs類。一般情況下,使用.NET自帶的類足夠了,但爲了說明原理,我們自定義一個事件參數類。

 試一試:使用事件參數

//事件參數
public class NumberOfThiefEventArgs : EventArgs
{
    public int numberOfThief;
//<strong>構造函數</strong>
public NumberOfThiefEventArgs(int number)
{
    numberOfThief = number;
}

}

//事件發送者
class Dog
{
//1.聲明關於事件的委託;
public delegate void AlarmEventHandler(object sender, NumberOfThiefEventArgs e);

//2.聲明事件;
public event AlarmEventHandler Alarm;

//3.編寫引發事件的函數,注意多了個參數
public void OnAlarm(NumberOfThiefEventArgs e)
{
if (this.Alarm != null)
{
Console.WriteLine("/n狗報警: 有小偷進來了,汪汪~~~~~~~/n");
this.Alarm(this, e);
}
}
}

//事件接收者
class Host
{
//4.編寫事件處理程序,參數中包含着numberOfThief信息
void HostHandleAlarm(object sender, NumberOfThiefEventArgs e)
{
if (e.numberOfThief <= 1)
{
Console.WriteLine(“主 人: 抓住了小偷!”);
}
else
{
Console.WriteLine(“主 人:打110報警,我家來了{0}個小偷!”, e.numberOfThief);
}
}

<strong>//5.註冊事件處理程序</strong>
public Host(Dog dog)
{
    dog.Alarm += new <strong><span style="text-decoration:underline;">Dog.AlarmEventHandler</span>(<span style="text-decoration:underline;">HostHandleAlarm</span>)</strong>;
}

}

//6.現在來觸發事件
class Program
{
static void Main(string[] args)
{
Dog dog = new Dog();
Host host = new Host(dog);

    //當前時間,從2008年12月31日23:59:50開始計時
    DateTime now = new DateTime(2008, 12, 31, 23, 59, 50);
    DateTime midnight = new DateTime(2009, 1, 1, 0, 0, 0);

    //等待午夜的到來
    Console.WriteLine("時間一秒一秒地流逝... ");
    while (now &lt; midnight)
    {
        Console.WriteLine("當前時間: " + now);
        System.Threading.Thread.Sleep(1000); //程序暫停一秒
         now = now.AddSeconds(1);                //時間增加一秒
    }

    //午夜零點小偷到達,看門狗引發Alarm事件
    Console.WriteLine("/n月黑風高的午夜: " + now);
    Console.WriteLine("小偷悄悄地摸進了主人的屋內... ");

    //創建事件參數
    NumberOfThiefEventArgs e = new NumberOfThiefEventArgs(3);
    dog.OnAlarm(e);
}

}

運行結果如下:

 

       在修改過的代碼中,我們定義了一個名爲NumberOfThiefEventArgs的事件參數類,它繼承於EventArgs類。在該類中我們聲明瞭一個名爲numberOfThief的成員變量,用來記錄來了幾個小偷。當事件發生時,狗通過事件參數傳告訴主人具體信息。

 

-----------------------------------------------

C# 中的委託和事件

 

源碼下載:http://www.tracefact.net/SourceCode/Delegates-and-Events-in-CSharp.rar

將方法作爲方法的參數

我們先不管這個標題如何的繞口,也不管委託究竟是個什麼東西,來看下面這兩個最簡單的方法,它們不過是在屏幕上輸出一句問候的話語:

public void GreetPeople(string name) {
    // 做某些額外的事情,比如初始化之類,此處略
    EnglishGreeting(name);
}
public void EnglishGreeting(string name) {
    Console.WriteLine("Morning, " + name);
}

暫且不管這兩個方法有沒有什麼實際意義。GreetPeople用於向某人問好,當我們傳遞代表某人姓名的name參數,比如說“Jimmy”,進去的時候,在這個方法中,將調用EnglishGreeting方法,再次傳遞name參數,EnglishGreeting則用於向屏幕輸出 “Morning, Jimmy”。

現在假設這個程序需要進行全球化,哎呀,不好了,我是中國人,我不明白“Morning”是什麼意思,怎麼辦呢?好吧,我們再加個中文版的問候方法:

public void ChineseGreeting(string name){
    Console.WriteLine("早上好, " + name);
}

這時候,GreetPeople也需要改一改了,不然如何判斷到底用哪個版本的Greeting問候方法合適呢?在進行這個之前,我們最好再定義一個枚舉作爲判斷的依據:

public enum Language{
    English, Chinese
}

public void GreetPeople(string name, Language lang){
    //做某些額外的事情,比如初始化之類,此處略
    swith(lang){
        case Language.English:
           EnglishGreeting(name);
           break;
       case Language.Chinese:
           ChineseGreeting(name);
           break;
    }
}

OK,儘管這樣解決了問題,但我不說大家也很容易想到,這個解決方案的可擴展性很差,如果日後我們需要再添加韓文版、日文版,就不得不反覆修改枚舉和GreetPeople()方法,以適應新的需求。

在考慮新的解決方案之前,我們先看看 GreetPeople的方法簽名:

public void GreetPeople(string name, Language lang)

我們僅看 string name,在這裏,string 是參數類型,name 是參數變量,當我們賦給name字符串“jimmy”時,它就代表“jimmy”這個值;當我們賦給它“張子陽”時,它又代表着“張子陽”這個值。然後,我們可以在方法體內對這個name進行其他操作。哎,這簡直是廢話麼,剛學程序就知道了。

如果你再仔細想想,假如GreetPeople()方法可以接受一個參數變量,這個變量可以代表另一個方法,當我們給這個變量賦值 EnglishGreeting的時候,它代表着 EnglsihGreeting() 這個方法;當我們給它賦值ChineseGreeting 的時候,它又代表着ChineseGreeting()方法。我們將這個參數變量命名爲 MakeGreeting,那麼不是可以如同給name賦值時一樣,在調用 GreetPeople()方法的時候,給這個MakeGreeting 參數也賦上值麼(ChineseGreeting或者EnglsihGreeting等)?然後,我們在方法體內,也可以像使用別的參數一樣使用MakeGreeting。但是,由於MakeGreeting代表着一個方法,它的使用方式應該和它被賦的方法(比如ChineseGreeting)是一樣的,比如:

MakeGreeting(name);

好了,有了思路了,我們現在就來改改GreetPeople()方法,那麼它應該是這個樣子了:

public void GreetPeople(string name, *** MakeGreeting){
    MakeGreeting(name);
}

注意到 *** ,這個位置通常放置的應該是參數的類型,但到目前爲止,我們僅僅是想到應該有個可以代表方法的參數,並按這個思路去改寫GreetPeople方法,現在就出現了一個大問題:這個代表着方法的MakeGreeting參數應該是什麼類型的?

NOTE:這裏已不再需要枚舉了,因爲在給MakeGreeting賦值的時候動態地決定使用哪個方法,是ChineseGreeting還是 EnglishGreeting,而在這個兩個方法內部,已經對使用“morning”還是“早上好”作了區分。

聰明的你應該已經想到了,現在是委託該出場的時候了,但講述委託之前,我們再看看MakeGreeting參數所能代表的 ChineseGreeting()和EnglishGreeting()方法的簽名:

public void EnglishGreeting(string name)
public void ChineseGreeting(string name)

如同name可以接受String類型的“true”和“1”,但不能接受bool類型的true和int類型的1一樣。MakeGreeting的 參數類型定義 應該能夠確定 MakeGreeting可以代表的方法種類,再進一步講,就是MakeGreeting可以代表的方法 的 參數類型和返回類型。

於是,委託出現了:它定義了MakeGreeting參數所能代表的方法的種類,也就是MakeGreeting參數的類型。

NOTE:如果上面這句話比較繞口,我把它翻譯成這樣:string 定義了name參數所能代表的值的種類,也就是name參數的類型。

本例中委託的定義:

public delegate void GreetingDelegate(string name);

可以與上面EnglishGreeting()方法的簽名對比一下,除了加入了delegate關鍵字以外,其餘的是不是完全一樣?

現在,讓我們再次改動GreetPeople()方法,如下所示:

public void GreetPeople(string name, GreetingDelegate MakeGreeting){
    MakeGreeting(name);
}

如你所見,委託GreetingDelegate出現的位置與 string相同,string是一個類型,那麼GreetingDelegate應該也是一個類型,或者叫類(Class)。但是委託的聲明方式和類卻完全不同,這是怎麼一回事?實際上,委託在編譯的時候確實會編譯成類。因爲Delegate是一個類,所以在任何可以聲明類的地方都可以聲明委託。更多的內容將在下面講述,現在,請看看這個範例的完整代碼:

using System;
using System.Collections.Generic;
using System.Text;

namespace Delegate {
     //定義委託,它定義了可以代表的方法的類型
     public delegate void GreetingDelegate(string name);
        class Program {

           private static void EnglishGreeting(string name) {
               Console.WriteLine("Morning, " + name);
           }

           private static void ChineseGreeting(string name) {
               Console.WriteLine("早上好, " + name);
           }

           //注意此方法,它接受一個GreetingDelegate類型的方法作爲參數
           private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {
               MakeGreeting(name);
            }

           static void Main(string[] args) {
               GreetPeople("Jimmy Zhang", EnglishGreeting);
               GreetPeople("張子陽", ChineseGreeting);
               Console.ReadKey();
           }
        }
    }

輸出如下:
Morning, Jimmy Zhang
早上好, 張子陽

我們現在對委託做一個總結:

委託是一個類,它定義了方法的類型,使得可以將方法當作另一個方法的參數來進行傳遞,這種將方法動態地賦給參數的做法,可以避免在程序中大量使用If-Else(Switch)語句,同時使得程序具有更好的可擴展性。

將方法綁定到委託

看到這裏,是不是有那麼點如夢初醒的感覺?於是,你是不是在想:在上面的例子中,我不一定要直接在GreetPeople()方法中給 name參數賦值,我可以像這樣使用變量:

static void Main(string[] args) {
    string name1, name2;
    name1 = "Jimmy Zhang";
    name2 = "張子陽"; 

     GreetPeople(name1, EnglishGreeting);
     GreetPeople(name2, ChineseGreeting);
    Console.ReadKey();
}

而既然委託GreetingDelegate 和 類型 string 的地位一樣,都是定義了一種參數類型,那麼,我是不是也可以這麼使用委託?

static void Main(string[] args) {
    GreetingDelegate delegate1, delegate2;
    delegate1 = EnglishGreeting;
    delegate2 = ChineseGreeting;

    GreetPeople("Jimmy Zhang", delegate1);
        GreetPeople("張子陽", delegate2);
        Console.ReadKey();
}

如你所料,這樣是沒有問題的,程序一如預料的那樣輸出。這裏,我想說的是委託不同於string的一個特性:可以將多個方法賦給同一個委託,或者叫將多個方法綁定到同一個委託,當調用這個委託的時候,將依次調用其所綁定的方法。在這個例子中,語法如下:

static void Main(string[] args) {
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting; // 先給委託類型的變量賦值
    delegate1 += ChineseGreeting;   // 給此委託變量再綁定一個方法

     // 將先後調用 EnglishGreeting 與 ChineseGreeting 方法
    GreetPeople("Jimmy Zhang", delegate1);  
    Console.ReadKey();
}

輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang

實際上,我們可以也可以繞過GreetPeople方法,通過委託來直接調用EnglishGreeting和ChineseGreeting:

static void Main(string[] args) {
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting; // 先給委託類型的變量賦值
    delegate1 += ChineseGreeting;   // 給此委託變量再綁定一個方法

    // 將先後調用 EnglishGreeting 與 ChineseGreeting 方法
    delegate1 ("Jimmy Zhang");   
    Console.ReadKey();
}

 

NOTE:這在本例中是沒有問題的,但回頭看下上面GreetPeople()的定義,在它之中可以做一些對於EnglshihGreeting和ChineseGreeting來說都需要進行的工作,爲了簡便我做了省略。

注意這裏,第一次用的“=”,是賦值的語法;第二次,用的是“+=”,是綁定的語法。如果第一次就使用“+=”,將出現“使用了未賦值的局部變量”的編譯錯誤。

我們也可以使用下面的代碼來這樣簡化這一過程:

GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
delegate1 += ChineseGreeting;   // 給此委託變量再綁定一個方法

看到這裏,應該注意到,這段代碼第一條語句與實例化一個類是何其的相似,你不禁想到:上面第一次綁定委託時不可以使用“+=”的編譯錯誤,或許可以用這樣的方法來避免:

GreetingDelegate delegate1 = new GreetingDelegate();
delegate1 += EnglishGreeting;   // 這次用的是 “+=”,綁定語法。
delegate1 += ChineseGreeting;   // 給此委託變量再綁定一個方法

但實際上,這樣會出現編譯錯誤: “GreetingDelegate”方法沒有采用“0”個參數的重載。儘管這樣的結果讓我們覺得有點沮喪,但是編譯的提示:“沒有0個參數的重載”再次讓我們聯想到了類的構造函數。我知道你一定按捺不住想探個究竟,但再此之前,我們需要先把基礎知識和應用介紹完。

既然給委託可以綁定一個方法,那麼也應該有辦法取消對方法的綁定,很容易想到,這個語法是“-=”:

static void Main(string[] args) {
    GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);
    delegate1 += ChineseGreeting;   // 給此委託變量再綁定一個方法

    // 將先後調用 EnglishGreeting 與 ChineseGreeting 方法
    GreetPeople("Jimmy Zhang", delegate1);  
    Console.WriteLine();

    delegate1 -= EnglishGreeting; //取消對EnglishGreeting方法的綁定
    // 將僅調用 ChineseGreeting 
    GreetPeople("張子陽", delegate1); 
    Console.ReadKey();
}
輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang
早上好, 張子陽

讓我們再次對委託作個總結:

使用委託可以將多個方法綁定到同一個委託變量,當調用此變量時(這裏用“調用”這個詞,是因爲此變量代表一個方法),可以依次調用所有綁定的方法。

事件的由來

我們繼續思考上面的程序:上面的三個方法都定義在Programe類中,這樣做是爲了理解的方便,實際應用中,通常都是 GreetPeople 在一個類中,ChineseGreeting和 EnglishGreeting 在另外的類中。現在你已經對委託有了初步瞭解,是時候對上面的例子做個改進了。假設我們將GreetingPeople()放在一個叫GreetingManager的類中,那麼新程序應該是這個樣子的:

namespace Delegate {
    //定義委託,它定義了可以代表的方法的類型
    public delegate void GreetingDelegate(string name);
    
    //新建的GreetingManager類
    public class GreetingManager{
       public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
           MakeGreeting(name);
       }
    }

    class Program {
       private static void EnglishGreeting(string name) {
           Console.WriteLine("Morning, " + name);
       }

       private static void ChineseGreeting(string name) {
           Console.WriteLine("早上好, " + name);
       }

       static void Main(string[] args) {
           // ... ...
        }
    }
}

這個時候,如果要實現前面演示的輸出效果,Main方法我想應該是這樣的:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.GreetPeople("Jimmy Zhang", EnglishGreeting);
    gm.GreetPeople("張子陽", ChineseGreeting);
}

我們運行這段代碼,嗯,沒有任何問題。程序一如預料地那樣輸出了:

Morning, Jimmy Zhang

早上好, 張子陽

現在,假設我們需要使用上一節學到的知識,將多個方法綁定到同一個委託變量,該如何做呢?讓我們再次改寫代碼:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    GreetingDelegate delegate1;
    delegate1 = EnglishGreeting;
    delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang", delegate1);
}

輸出:
Morning, Jimmy Zhang
早上好, Jimmy Zhang

到了這裏,我們不禁想到:面向對象設計,講究的是對象的封裝,既然可以聲明委託類型的變量(在上例中是delegate1),我們何不將這個變量封裝到 GreetManager類中?在這個類的客戶端中使用不是更方便麼?於是,我們改寫GreetManager類,像這樣:

public class GreetingManager{
    //在GreetingManager類的內部聲明delegate1變量
    public GreetingDelegate delegate1;  

    public void GreetPeople(string name, GreetingDelegate MakeGreeting) {
       MakeGreeting(name);
    }
}

現在,我們可以這樣使用這個委託變量:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang", gm.delegate1);
}

輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang

儘管這樣做沒有任何問題,但我們發現這條語句很奇怪。在調用gm.GreetPeople方法的時候,再次傳遞了gm的delegate1字段:

gm.GreetPeople("Jimmy Zhang", gm.delegate1);

既然如此,我們何不修改 GreetingManager 類成這樣:

public class GreetingManager{
    //在GreetingManager類的內部聲明delegate1變量
    public GreetingDelegate delegate1;  

    public void GreetPeople(string name) {
        if(delegate1!=null){     //如果有方法註冊委託變量
          delegate1(name);      //通過委託調用方法
       }
    }
}

在客戶端,調用看上去更簡潔一些:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.delegate1 = EnglishGreeting;
    gm.delegate1 += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang");      //注意,這次不需要再傳遞 delegate1變量
}

輸出爲:
Morning, Jimmy Zhang
早上好, Jimmy Zhang

儘管這樣達到了我們要的效果,但是還是存在着問題:

在這裏,delegate1和我們平時用的string類型的變量沒有什麼分別,而我們知道,並不是所有的字段都應該聲明成public,合適的做法是應該public的時候public,應該private的時候private。

我們先看看如果把 delegate1 聲明爲 private會怎樣?結果就是:這簡直就是在搞笑。因爲聲明委託的目的就是爲了把它暴露在類的客戶端進行方法的註冊,你把它聲明爲private了,客戶端對它根本就不可見,那它還有什麼用?

再看看把delegate1 聲明爲 public 會怎樣?結果就是:在客戶端可以對它進行隨意的賦值等操作,嚴重破壞對象的封裝性。

最後,第一個方法註冊用“=”,是賦值語法,因爲要進行實例化,第二個方法註冊則用的是“+=”。但是,不管是賦值還是註冊,都是將方法綁定到委託上,除了調用時先後順序不同,再沒有任何的分別,這樣不是讓人覺得很彆扭麼?

現在我們想想,如果delegate1不是一個委託類型,而是一個string類型,你會怎麼做?答案是使用屬性對字段進行封裝。

於是,Event出場了,它封裝了委託類型的變量,使得:在類的內部,不管你聲明它是public還是protected,它總是private的。在類的外部,註冊“+=”和註銷“-=”的訪問限定符與你在聲明事件時使用的訪問符相同。

我們改寫GreetingManager類,它變成了這個樣子:

public class GreetingManager{
    //這一次我們在這裏聲明一個事件
    public event GreetingDelegate MakeGreet;

    public void GreetPeople(string name) {
        MakeGreet(name);
    }
}

很容易注意到:MakeGreet 事件的聲明與之前委託變量delegate1的聲明唯一的區別是多了一個event關鍵字。看到這裏,在結合上面的講解,你應該明白到:事件其實沒什麼不好理解的,聲明一個事件不過類似於聲明一個進行了封裝的委託類型的變量而已。

爲了證明上面的推論,如果我們像下面這樣改寫Main方法:

static void Main(string[] args) {
    GreetingManager gm = new  GreetingManager();
    gm.MakeGreet = EnglishGreeting;         // 編譯錯誤1
    gm.MakeGreet += ChineseGreeting;

    gm.GreetPeople("Jimmy Zhang");
}

會得到編譯錯誤:事件“Delegate.GreetingManager.MakeGreet”只能出現在 += 或 -= 的左邊(從類型“Delegate.GreetingManager”中使用時除外)。

事件和委託的編譯代碼

這時候,我們註釋掉編譯錯誤的行,然後重新進行編譯,再借助Reflactor來對 event的聲明語句做一探究,看看爲什麼會發生這樣的錯誤:

public event GreetingDelegate MakeGreet;

可以看到,實際上儘管我們在GreetingManager裏將 MakeGreet 聲明爲public,但是,實際上MakeGreet會被編譯成 私有字段,難怪會發生上面的編譯錯誤了,因爲它根本就不允許在GreetingManager類的外面以賦值的方式訪問,從而驗證了我們上面所做的推論。

我們再進一步看下MakeGreet所產生的代碼:

private GreetingDelegate MakeGreet; //對事件的聲明 實際是 聲明一個私有的委託變量
 
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value);
}

[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_MakeGreet(GreetingDelegate value){
    this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value);
}

現在已經很明確了:MakeGreet事件確實是一個GreetingDelegate類型的委託,只不過不管是不是聲明爲public,它總是被聲明爲private。另外,它還有兩個方法,分別是add_MakeGreet和remove_MakeGreet,這兩個方法分別用於註冊委託類型的方法和取消註冊。實際上也就是: “+= ”對應 add_MakeGreet,“-=”對應remove_MakeGreet。而這兩個方法的訪問限制取決於聲明事件時的訪問限制符。

在add_MakeGreet()方法內部,實際上調用了System.Delegate的Combine()靜態方法,這個方法用於將當前的變量添加到委託鏈表中。我們前面提到過兩次,說委託實際上是一個類,在我們定義委託的時候:

public delegate void GreetingDelegate(string name);

當編譯器遇到這段代碼的時候,會生成下面這樣一個完整的類:

public sealed class GreetingDelegate:System.MulticastDelegate{
    public GreetingDelegate(object @object, IntPtr method);
    public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object);
    public virtual void EndInvoke(IAsyncResult result);
    public virtual void Invoke(string name);
}

 

 

關於這個類的更深入內容,可以參閱《CLR Via C#》等相關書籍,這裏就不再討論了。

委託、事件與Observer設計模式

範例說明

上面的例子已不足以再進行下面的講解了,我們來看一個新的範例,因爲之前已經介紹了很多的內容,所以本節的進度會稍微快一些:

假設我們有個高檔的熱水器,我們給它通上電,當水溫超過95度的時候:1、揚聲器會開始發出語音,告訴你水的溫度;2、液晶屏也會改變水溫的顯示,來提示水已經快燒開了。

現在我們需要寫個程序來模擬這個燒水的過程,我們將定義一個類來代表熱水器,我們管它叫:Heater,它有代表水溫的字段,叫做temperature;當然,還有必不可少的給水加熱方法BoilWater(),一個發出語音警報的方法MakeAlert(),一個顯示水溫的方法,ShowMsg()。

namespace Delegate {
    class Heater {
    private int temperature; // 水溫
    // 燒水
    public void BoilWater() {
        for (int i = 0; i <= 100; i++) {
           temperature = i;

           if (temperature > 95) {
               MakeAlert(temperature);
               ShowMsg(temperature);
            }
        }
    }

    // 發出語音警報
    private void MakeAlert(int param) {
       Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:" , param);
    }
    
    // 顯示水溫
    private void ShowMsg(int param) {
       Console.WriteLine("Display:水快開了,當前溫度:{0}度。" , param);
    }
}

class Program {
    static void Main() {
       Heater ht = new Heater();
       ht.BoilWater();
    }
}
}

Observer設計模式簡介

上面的例子顯然能完成我們之前描述的工作,但是卻並不夠好。現在假設熱水器由三部分組成:熱水器、警報器、顯示器,它們來自於不同廠商並進行了組裝。那麼,應該是熱水器僅僅負責燒水,它不能發出警報也不能顯示水溫;在水燒開時由警報器發出警報、顯示器顯示提示和水溫。

這時候,上面的例子就應該變成這個樣子:   

// 熱水器
public class Heater { 
    private int temperature;
        
    // 燒水
    private void BoilWater() {
       for (int i = 0; i <= 100; i++) {
           temperature = i;
        }
    }
}

// 警報器
public class Alarm{
    private void MakeAlert(int param) {
       Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:" , param);
    }
}

// 顯示器
public class Display{
    private void ShowMsg(int param) {
       Console.WriteLine("Display:水已燒開,當前溫度:{0}度。" , param);
    }
}

這裏就出現了一個問題:如何在水燒開的時候通知報警器和顯示器?在繼續進行之前,我們先了解一下Observer設計模式,Observer設計模式中主要包括如下兩類對象:

  1. Subject:監視對象,它往往包含着其他對象所感興趣的內容。在本範例中,熱水器就是一個監視對象,它包含的其他對象所感興趣的內容,就是temprature字段,當這個字段的值快到100時,會不斷把數據發給監視它的對象。
  2. Observer:監視者,它監視Subject,當Subject中的某件事發生的時候,會告知Observer,而Observer則會採取相應的行動。在本範例中,Observer有警報器和顯示器,它們採取的行動分別是發出警報和顯示水溫。

在本例中,事情發生的順序應該是這樣的:

  1. 警報器和顯示器告訴熱水器,它對它的溫度比較感興趣(註冊)。
  2. 熱水器知道後保留對警報器和顯示器的引用。
  3. 熱水器進行燒水這一動作,當水溫超過95度時,通過對警報器和顯示器的引用,自動調用警報器的MakeAlert()方法、顯示器的ShowMsg()方法。

類似這樣的例子是很多的,GOF對它進行了抽象,稱爲Observer設計模式:Observer設計模式是爲了定義對象間的一種一對多的依賴關係,以便於當一個對象的狀態改變時,其他依賴於它的對象會被自動告知並更新。Observer模式是一種鬆耦合的設計模式。

實現範例的Observer設計模式

我們之前已經對委託和事件介紹很多了,現在寫代碼應該很容易了,現在在這裏直接給出代碼,並在註釋中加以說明。

using System;
using System.Collections.Generic;
using System.Text;

namespace Delegate {
    // 熱水器
    public class Heater {
       private int temperature;
       public delegate void BoilHandler(int param);   //聲明委託
       public event BoilHandler BoilEvent;        //聲明事件

       // 燒水
       public void BoilWater() {
           for (int i = 0; i <= 100; i++) {
              temperature = i;

              if (temperature > 95) {
                  if (BoilEvent != null) { //如果有對象註冊
                      BoilEvent(temperature);  //調用所有註冊對象的方法
                  }
              }
           }
       }
    }

    // 警報器
    public class Alarm {
       public void MakeAlert(int param) {
           Console.WriteLine("Alarm:嘀嘀嘀,水已經 {0} 度了:", param);
       }
    }

    // 顯示器
    public class Display {
       public static void ShowMsg(int param) { //靜態方法
           Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", param);
       }
    }
    
    class Program {
       static void Main() {
           Heater heater = new Heater();
           Alarm alarm = new Alarm();

           heater.BoilEvent += alarm.MakeAlert;    //註冊方法
           heater.BoilEvent += (new Alarm()).MakeAlert;   //給匿名對象註冊方法
           heater.BoilEvent += Display.ShowMsg;       //註冊靜態方法

           heater.BoilWater();   //燒水,會自動調用註冊過對象的方法
       }
    }
}
輸出爲:
Alarm:嘀嘀嘀,水已經 96 度了:
Alarm:嘀嘀嘀,水已經 96 度了:
Display:水快燒開了,當前溫度:96度。
// 省略...

.Net Framework中的委託與事件

儘管上面的範例很好地完成了我們想要完成的工作,但是我們不僅疑惑:爲什麼.Net Framework 中的事件模型和上面的不同?爲什麼有很多的EventArgs參數?

在回答上面的問題之前,我們先搞懂 .Net Framework的編碼規範:

  • 委託類型的名稱都應該以EventHandler結束。
  • 委託的原型定義:有一個void返回值,並接受兩個輸入參數:一個Object 類型,一個 EventArgs類型(或繼承自EventArgs)。
  • 事件的命名爲 委託去掉 EventHandler之後剩餘的部分。
  • 繼承自EventArgs的類型應該以EventArgs結尾。

再做一下說明:

  1. 委託聲明原型中的Object類型的參數代表了Subject,也就是監視對象,在本例中是 Heater(熱水器)。回調函數(比如Alarm的MakeAlert)可以通過它訪問觸發事件的對象(Heater)。
  2. EventArgs 對象包含了Observer所感興趣的數據,在本例中是temperature。

上面這些其實不僅僅是爲了編碼規範而已,這樣也使得程序有更大的靈活性。比如說,如果我們不光想獲得熱水器的溫度,還想在Observer端(警報器或者顯示器)方法中獲得它的生產日期、型號、價格,那麼委託和方法的聲明都會變得很麻煩,而如果我們將熱水器的引用傳給警報器的方法,就可以在方法中直接訪問熱水器了。

現在我們改寫之前的範例,讓它符合 .Net Framework 的規範:

using System;
using System.Collections.Generic;
using System.Text;

namespace Delegate {
    // 熱水器
    public class Heater {
       private int temperature;
       public string type = "RealFire 001";       // 添加型號作爲演示
       public string area = "China Xian";         // 添加產地作爲演示
       //聲明委託
       public delegate void BoiledEventHandler(Object sender, BoiledEventArgs e);
       public event BoiledEventHandler Boiled; //聲明事件

       // 定義BoiledEventArgs類,傳遞給Observer所感興趣的信息
       public class BoiledEventArgs : EventArgs {
           public readonly int temperature;
           public BoiledEventArgs(int temperature) {
              this.temperature = temperature;
           }
       }

       // 可以供繼承自 Heater 的類重寫,以便繼承類拒絕其他對象對它的監視
       protected virtual void OnBoiled(BoiledEventArgs e) {
           if (Boiled != null) { // 如果有對象註冊
              Boiled(this, e);  // 調用所有註冊對象的方法
           }
       }
       
       // 燒水。
       public void BoilWater() {
           for (int i = 0; i <= 100; i++) {
              temperature = i;
              if (temperature > 95) {
                  //建立BoiledEventArgs 對象。
                  BoiledEventArgs e = new BoiledEventArgs(temperature);
                  OnBoiled(e);  // 調用 OnBolied方法
              }
           }
       }
    }

    // 警報器
    public class Alarm {
       public void MakeAlert(Object sender, Heater.BoiledEventArgs e) {
           Heater heater = (Heater)sender;     //這裏是不是很熟悉呢?
           //訪問 sender 中的公共字段
           Console.WriteLine("Alarm:{0} - {1}: ", heater.area, heater.type);
           Console.WriteLine("Alarm: 嘀嘀嘀,水已經 {0} 度了:", e.temperature);
           Console.WriteLine();
       }
    }

    // 顯示器
    public class Display {
       public static void ShowMsg(Object sender, Heater.BoiledEventArgs e) {   //靜態方法
           Heater heater = (Heater)sender;
           Console.WriteLine("Display:{0} - {1}: ", heater.area, heater.type);
           Console.WriteLine("Display:水快燒開了,當前溫度:{0}度。", e.temperature);
           Console.WriteLine();
       }
    }

    class Program {
       static void Main() {
           Heater heater = new Heater();
           Alarm alarm = new Alarm();

           heater.Boiled += alarm.MakeAlert;   //註冊方法
           heater.Boiled += (new Alarm()).MakeAlert;      //給匿名對象註冊方法
           heater.Boiled += newHeater.BoiledEventHandler(alarm.MakeAlert);    //也可以這麼註冊
           heater.Boiled += Display.ShowMsg;       //註冊靜態方法

           heater.BoilWater();   //燒水,會自動調用註冊過對象的方法
       }
    }
}

輸出爲:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Alarm:China Xian - RealFire 001:
Alarm: 嘀嘀嘀,水已經 96 度了:
Display:China Xian - RealFire 001:
Display:水快燒開了,當前溫度:96度。
// 省略 ...

C#中的委託和事件(續)

引言

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

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

在 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 += newNumberChangedEventHandler(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);
    }
}

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

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

就好像如果我們要定義一個數字類型,我們會使用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 += newGeneralEventHandler(sub1.OnNumberChanged);
        pub.NumberChanged += newGeneralEventHandler(sub2.OnNumberChanged);
        pub.NumberChanged += newGeneralEventHandler(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*/ }

如果運行這段代碼,得到的輸出是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()方法中,我們使用了賦值操作符“=”,而非“+=”,通過這種方式就避免了多個方法註冊。上面的代碼儘管可以完成我們的需要,但是此時大家還應該注意下面兩點:

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

2、我們還應該發現,這裏採用的、對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...

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

 

----------------------------------------------------------

C#委託之個人理解

 

什麼是委託
  首先要知道什麼是委託,用最通俗易懂的話來講,你就可以把委託看成是用來執行方法(函數)的一個東西。

如何使用委託
  在使用委託的時候,你可以像對待一個類一樣對待它。即先聲明,再實例化。只是有點不同,類在實例化之後叫對象或實例,但委託在實例化後仍叫委託。

聲明,如:

1    namespace Vczx.ProCSharp.Exc
2    {
3        delegate double MathsOp( double x );
4        //class defination here
5    }
  這就聲明瞭一個委託,意義:任何一個返回值爲double,且只有一個形參爲double的函數,都可以用這個委託來調用。
  注意:委託的聲明位置在namespace裏面,類的外面。其實,委託的聲明也可以在類的裏面,甚至是任何一個可以聲明類的地方。
  實例化:
  首先我們要先有一個滿足委託聲明的方法,假設一個返回一個數的2倍的方法:
1class MathsOperations
2{
3    public static double MultiplyBy2( double value )
4    {
5        return value * 2;
6    }
7}

  有了這樣一個方法,我們就可以實例化一個委託了:
MathsOp operation = new MathsOp( MathsOperations.MultiplyBy2 );
  在實例化一個委託時,要給它一個參數,這個參數就是委託執行的方法,它可以是靜態方法,也可以是實例方法(這一點有別於函數指針,函數指針只能調用靜態方法),如:
MathsOp operation = new MathsOp( new Class1().Method1 );

在實例化完一個委託之後,就可以用這個委託來調用方法了:
double result = operation( 1.23 );

例子代碼:


1namespace Vczx.ProCSharp.Exc
2{
3    delegate double MathsOp( double x );
4    class Start
5    {
6        public class MyDelegate
7        {
8            public static double MultiplyBy2( double x )
9            {
10                return x * 2;
11            }
12        }
13        [STAThread]
14        static void Main(string[] args)
15        {
16            MathsOp operation = new MathsOp( MyDelegate.MultiplyBy2 );
17            double x = 1.23;
18            double result = operation( x );
19            Console.WriteLine( "{0} multiply by 2 is {1}", x, result ); 
20            Console.Read();
21        }
22    }
23}

多路廣播委託
   前面使用的委託只包含一個方法調用。調用委託的次數與調用方法的次數相同。如果要調用多個方法,就需要多次顯示調用這個委託。其實委託也可以包含多個方法,這種委託就是多路廣播委託。多路廣播委託派生於System.MulticastDelegate,它的Combine方法允許把多個方法調用鏈接在一起,我們可以通過+=來向委託添加調用方法,也可以用-=刪除其中的調用方法。如:


1namespace Vczx.ProCSharp.Exc
2{
3    public class MyDelegate
4    {
5        public static void MultiplyBy2( double value )
6        {
7            double result = value * 2;
8            Console.WriteLine( "Multiplying by 2: {0} gives {1}", value, result );
9        }
10
11        public static void Squre( double value )
12        {
13            double result = value * value;
14            Console.WriteLine( "Squaring: {0} gives {1}", value, result );
15        }
16    }
17
18    delegate void MathsOp( double x );
19
20    class Start
21    {
22        [STAThread]
23        static void Main(string[] args)
24        {
25            MathsOp operation = new MathsOp( MyDelegate.MultiplyBy2 );
26            operation += new MathsOp( MyDelegate.Squre );
27            double x = 1.23;
28            operation( x );
29
30            operation -= new MathsOp( MyDelegate.MultiplyBy2 );
31            operation( x );
32            
33            Console.Read();
34        }
35    }
36}
輸出:
Multiplying by 2: 1.23 gives 2.46
Squaring: 1.23 gives 1.5129
Squaring: 1.23 gives 1.5129

  注意,多路廣播委託聲明時必須返回void,否則返回值不知道應該送回什麼地方。對此,我做了一個測試:如果不將委託的聲明返回void,則返回值返回的是最後一個鏈入委託鏈的方法的返回值,編譯不會出錯。


爲什麼要用委託
  使用委託使程序員可以將方法引用封裝在委託對象內。然後可以將該委託對象傳遞給可調用所引用方法的代碼,而不必在編譯時知道將調用哪個方法。與C或C++中的函數指針不同,委託是面向對象,而且是類型安全的。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章