把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