多播委託,事件

分類:

  把C#中的委託(Delegate)和事件(Event)放到現在講是有目的的:給下次寫的設計模式——觀察者(Observer)有一個參考。
 
  委託和事件應該是C#相較於C++等之前的非託管的語言提出的一個新的術語(term)。“舊瓶裝新酒”這樣的描述似乎有些“貶義”,但確實是這樣。委託也好,事件也好最初的起源是C/C++中的函數指針,關於函數指針的簡單介紹可以參見我以前的一篇《C/C++中指向函數的指針》。不過舊瓶裝新酒沒有什麼不好,反而給人添加了許多新滋味。
 
1. Function pointer--the origin of delegates and events .
 
 
  書回正傳,既然函數指針是它們(委託和事件)的起源。那我們先看看什麼情況下我們需要函數指針。函數指針最常用的方式就是回調(callback)——在函數休內回調主函數裏的函數。有些繞口,看代碼:
 

//Platform: WinXP + VC6.0
#include <iostream.h>
#include <list>
using namespace std;

void max(int a, int b)
{
    cout<<"now call max("<<a<<","<<b<<")..."<<endl;
    int t = a>b?a:b;
    cout<<t<<endl;
}
void min(int a, int b)
{
    cout<<"now call min("<<a<<","<<b<<")..."<<endl;
    int t = a<b?a:b;
    cout<<t<<endl;
}
typedef void (*myFun)(int a, int b); //定義一個函數指針用來引用max,min


//回調函數
void callback(myFun fun, int a, int b)
{
    fun(a,b);
}
void main()
{
    int i = 10;
    int j = 55;
    callback(max,i,j); 

    callback(min,i,j);
}

 
Output:
now call max(10,55)...
55
now call min(10,55)...
10
Press any key to continue

  輸出的結果有可能另一些對函數指針不熟悉的朋友我些意外,我們並沒在main()中顯式調用max(),min()呀,怎麼會調用到它們呢。再仔細檢查一下:你可能發現了:
 
  callback(max,i,j); 
 
這個函數調用了max(),這下好了。你便可以回答類似於這樣的問題:我怎麼在一個函數(callback)體內調用[主調用函數中的函數(max或min)],最好能通過參數指入具體需要指定哪一個函數?
 
  這便是函數指針的作用了,通過轉入函數指針,可以很方便的回調(callback)另外一些函數,而且可以實現參觀化具體需要回調用的函數。
 
  2,Introduce delegate in c#;
 
 
  .net裏一向是"忌諱"提及"指針"的,"指針"很多程度上意味着不安全。C#.net裏便提出了一個新的術語:委託(delegate)來實現類似函數指針的功能。我們來看看在C#中怎麼樣實現上面的例子。
 

//Platform: WinXP + C# in vs2003
using System;
namespace Class1
{ 
    class ExcelProgram
    {
        static void max(int a, int b)
        {
            Console.WriteLine("now call max({0},{1})",a,b);
            int t = a>b?a:b;
            Console.WriteLine(t);
        }
        static void min(int a, int b)
        {
            Console.WriteLine("now call min({0},{1})",a,b);
            int t = a<b?a:b;
            Console.WriteLine(t);
        }
        delegate void myFun(int a, int b); //定義一個委託用來引用max,min

        //回調函數
        static void callback(myFun fun, int a, int b)
        {
            fun(a,b);
        }
        [STAThread]
        static void Main(string[] args)
        {
            int i = 10;
            int j = 55;
            callback(new myFun(max),i,j); 
            callback(new myFun(min),i,j);
            Console.ReadLine();
        }
    }
}

 
其實代碼上大同小異,除了幾個static申明以外(C#除靜態成員外必須要求對象引用),最大的變化要算定義"函數指指",哦...不..不..不..應該是定義"委託"(小樣穿上馬甲了..). 定義委託的語法如下:
 
     delegate void  myFun(int a, int b);     //定義一個委託用來引用max,min
 
其中delegate是關鍵字,myFun是委託名,剩下的是函數簽名(signature).我們可以申明一個委託:
 
   myFun Max = new myFun(max);
 
那麼上面的回調函數的代碼便可以寫成:callback(Max,i,j);
 
3, Difference between function pointer and delegate;
 
 
  委託除了可以引用一個函數外,能力上還有了一些加強,其中有一點不得不提的是:多點委託(Multicast delegate).簡單地講就是可以通過一個申明一個委託,來調用多個函數,不信?我們只要稍微更改一下上面的C#代碼中的Main函數就可以了,類似:
 
  static void Main(string[] args)
  {
   int i = 10;
   int j = 55;
 
   myFun mulCast = new myFun(max);
   mulCast += new myFun(min);      
//(1)
 
   callback(mulCast,i,j);   
   //callback(new myFun(min),i,j);
   Console.ReadLine();
  }
 
輸出如下:
now call max(10,55)...
55
now call min(10,55)...
10
Press any key to continue
 
  沒騙你吧,我們只用了一個委託mulCast便同時調用了max和min。不知你注意到沒有,上面代碼的(1)處用"+="給已經存在的委託(mulCast)又加了一個函數(min)。這樣看來C#中的委託更像一個函數指針鏈表。實質是在C#中,delegate關鍵字指定的委託自動從System.MulticastDelegate派生.而System.MulticastDelegate是一個帶有鏈接的委託列表,在callback中只需調用mulCast的引用便可以以同樣的參數調用該鏈表中的所有函數。
 
  如果還是覺得不過隱,那我們就繼續,下圖展示了剛纔那段C#代碼的IL(用ILDasm反彙編即可):
 
 
 
在C#中委託是作爲一個特殊的類型(Type,Object)來對待的,委託對象也有自己的成員:BeginInvoke,EndInvoke,Invoke。這幾個成員是你定義一個委託時編譯器幫你自動自成的,而且他們都是virtual函數,具體函數體由runtime來實現。我們雙擊一個callback,可以看見以下IL:
{
  // 代碼大小       9 (0x9)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  ldarg.2
  IL_0003:  callvirt   instance void Class1.ExcelProgram/myFun::Invoke(int32,
                                                                       int32)
  IL_0008:  ret
} // end of method ExcelProgram::callback
 
從這段IL我們可以看出,當我們使用語句:fun(a,b)時,調用的卻是委託對象(即然委託是類型,那麼他自也就會有對象)的myFun::Invoke().該委託對象(即上面的mulCast)通過調用Invoke來調用對象本身所關係的函數引用。
 
  那我們再看看,一個委託對象是怎麼樣關聯到函數的呢,我們雙擊Main函數,可以看到以下IL,雖然IL語法複雜但仍不影響我們瞭解它是怎麼樣將一個委託關聯到一個(或多個)函數的引用的。
 
.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) 
  // 代碼大小       58 (0x3a)
  .maxstack  4
  .locals ([0] int32 i,
           [1] int32 j,
           [2] class Class1.ExcelProgram/myFun mulCast)
  IL_0000:  ldc.i4.s   10
  IL_0002:  stloc.0
  IL_0003:  ldc.i4.s   55
  IL_0005:  stloc.1
  IL_0006:  ldnull
  IL_0007:  ldftn      void Class1.ExcelProgram::max(int32,
                                                     int32)
  IL_000d:  newobj     instance void Class1.ExcelProgram/myFun::.ctor(object,
                                                                      native int)
  IL_0012:  
stloc.2
  IL_0013:  ldloc.2
  IL_0014:  ldnull
  IL_0015:  ldftn      void Class1.ExcelProgram::min(int32,
                                                     int32)
  IL_001b:  newobj     instance void Class1.ExcelProgram/myFun::.ctor(object,
                                                                      native int)
  IL_0020:  call       class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(class [mscorlib]System.Delegate,
                                                                                          class [mscorlib]System.Delegate)
  IL_0025:  castclass  Class1.ExcelProgram/myFun
  IL_002a:  stloc.2
  IL_002b:  ldloc.2
  IL_002c:  ldloc.0
  IL_002d:  ldloc.1
  IL_002e:  call       void Class1.ExcelProgram::callback(class Class1.ExcelProgram/myFun,
                                                          int32,
                                                          int32)
  IL_0033:  call       string [mscorlib]System.Console::ReadLine()
  IL_0038:  pop
  IL_0039:  ret
} // end of method ExcelProgram::Main
 
從上面的IL可以看出對於語句:
  myFun mulCast = new myFun(max);
是通過以max作爲參數構建一個委託對象mulCast。但對於語句:
  mulCast += new myFun(min);
等價於(你甚至可以用下面的語句代碼上面的:mulCast += new myFun(min)):
  mulCast = (myFun) Delegate.Combine(mulCast, new myFun(min));
 
  哦,原來是通過調用Delegate.Combine的靜態方法將mulCast和min函數進行關聯,Delegate.Combine方法只是簡單地將min函數的引用加至委託對象mulCast的函數引用列表中。
 
  4,Introduce event;
 
 
  事件/消息機制是Windows的核心,其實提供事件功能的卻是函數指針,你信麼?接下來我們再看看C#事件(Event).在C#中事件是一類特殊的委託.
  一個類提供了"事件",那麼他至少提供了以下字段/方法:
 
  一個委託類型的字段(field),用來保存一旦事件時通知哪些對象。即通知所有訂閱該事件的對象.別忘記C#中委託是支持多播的。
  兩個方法,以委託類型爲參數。作用是將訂閱該事件的對象方法加至上面的委託類型字段中,以便事件發生後可以通過調用該方法來通知對象事件已發生。
 
  我們簡單地定義一個類Test,該類支持事件:
  class Test
  {
   public event EventHandler OnClick;
 
   public void GenEvent(EventArgs e)  //引發事件方法
   {
    EventHandler temp = OnClick;  
    //通知所有已訂閱事件的對象
    if(temp != null)
     temp(this,e);  
   }
  
  }
 
我們反彙編這段代碼,如下圖:
 
 
  簡單地定義一個字段哪來的那麼多方法?其實這都是編譯器幫你加上去的。當你定義一個事件時,編譯器爲了實現事件的功能會自動加上兩個方法來提供“訂閱”和“取消訂閱”的功能。
  通過下面的語法,你便可以訂閱事件:
  test.OnClick +=new EventHandler(test_OnClick);
  也就是說,一旦test事件發生時(通過調用test.GenEvent()方法)。test便會調用註冊到OnClick上的方法。來通知所有訂閱該事件的對象。
 
  訂閱是什麼?“訂閱就是調用定義事件時自動生成的add_OnClick.”“那取消訂閱就是調用定義事件時自動生成的remove_OnClick”,恭喜你!都學會搶答了.對於上面的訂閱事件語句,邏輯意義上等同於:
  test.add_OnClick(new EventHandler(test_OnClick));
但C#並不能直接調用該方法,只能通過 "+=" 來實現。來看IL:

  IL_003b:  ldftn      void Class1.ExcelProgram::test_OnClick(object,
                                                              class [mscorlib]System.EventArgs)  //先將test_OnClick壓棧
  IL_0041:  newobj     instance void [mscorlib]System.EventHandler::.ctor(object,
                                                                         native int)              //new一個委託對對象
  IL_0046:  callvirt   instance void Class1.ExcelProgram/Test::add_OnClick(class [mscorlib]System.EventHandler)   //通過調用add_OnClick方法將上面生委託加至test的事件(委託列表)中.
 
 
  5,summarize.
 
  如果對設計模式中的觀察者模式較爲熟悉的話。其實支持事件的類也就是觀察者模式中的Subject(主題,我個人比較喜歡這麼譯).而所有訂閱事件的對象構成了Observers.
 
  最後來句總結吧,總結也許不嚴謹,但提供理解那還是絕佳滴..我騙你..(鼻子又變長了).....
  "委託"是"函數指針"鏈表,當然該鏈表也可以只有一個元素,如果這樣的話:"委託" 約等於 "函數指針";
  "事件"是一類特特殊的"委託",你定義一個"事件",表示你同時定義了:一個委託+兩個方法。
 
  後記:如果還不理解事件,先不要急,說不定你先把它忘記不想,等會一閃光,你就會理解了。或者你等着我下一篇《設計模式----觀察者(Observer)》,我想等你看完設計模式中的觀察者之後再回來看"事件",看"多播委託(MulticastDelegate)"應該可以:忽然開朗。
 
  如果還覺得不過隱。下面給出一個很好的幫助理解的例子,來自Jeffrey Richter.希望我的註解能幫上些忙:
  

using System;
using System.Text;
using System.Data;

namespace Class1
{    
    //定義事件引發時,需要傳的參數
    class NewMailEventArgs:EventArgs
    {
        private readonly string m_from;
        private readonly string m_to;
        private readonly string m_subject;
        public NewMailEventArgs(string from, string to, string subject)
        {
            m_from = from;
            m_to = to;
            m_subject = subject;
        }
        public string From
        {
            get{return m_from;}
        }
        public string To
        {
            get{return m_to;}
        }
        public string Subject
        {
            get{return m_subject;}
        }

    }

    //事件所用的委託(鏈表)
    delegate void NewMailEventHandler(object sender, NewMailEventArgs e);

    //提供事件的類
    class MailManager
    {
        public event NewMailEventHandler NewMail;
        //通知已訂閱事件的對象
        protected virtual void OnNewMail(NewMailEventArgs e)
        {
            NewMailEventHandler temp = NewMail; //MulticastDelegate一個委託鏈表
            //通知所有已訂閱事件的對象
            if(temp != null)
                temp(this,e); 
//通過事件NewMail(一種特殊的委託)逐一回調客戶端的方法

        }
        //提供一個方法,引發事件
        public void SimulateNewMail(string from, string to, string subject)
        {
            NewMailEventArgs e = new NewMailEventArgs(from,to,subject);
            OnNewMail(e);
        }
    }


    //使用事件
    class Fax
    {
        public Fax(MailManager mm)
        {
            //Subscribe 
            mm.NewMail += new NewMailEventHandler(Fax_NewMail);
        }
        private void Fax_NewMail(object sender, NewMailEventArgs e)
        {
            Console.WriteLine("Message arrived at Fax...");
            Console.WriteLine("From={0}, To={1}, Subject='{2}'",e.From,e.To,e.Subject);
        }
        public void Unregister(MailManager mm)
        {
            mm.NewMail -= new NewMailEventHandler(Fax_NewMail);
        }
    }
    class Print
    {
        public Print(MailManager mm)
        {
            //Subscribe ,在mm.NewMail的委託鏈表中加入Print_NewMail方法
            mm.NewMail += new NewMailEventHandler(Print_NewMail);
        }
        private void Print_NewMail(object sender, NewMailEventArgs e)
        {
            Console.WriteLine("Message arrived at Print...");
            Console.WriteLine("From={0}, To={1}, Subject='{2}'",e.From,e.To,e.Subject);
        }
        public void Unregister(MailManager mm)
        {
            mm.NewMail -= new NewMailEventHandler(Print_NewMail);
        }
    }

    class ExcelProgram
    {
        [STAThread]
        static void Main(string[] args)
        {    
            MailManager mm = new MailManager();
            if(true)
            {
                Fax fax = new Fax(mm);
                Print prt = new Print(mm);
            }

            mm.SimulateNewMail("Anco","Jerry","Event test");
            Console.ReadLine();
        }
    }
}


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