事件的由來
上文說到委托的安全性不佳,於是我們要將委托本身私有化,但還要暴露若干方法讓外界使用。其中最重要的必然就是為委托掛接方法和調用委托,以便間接地調用委托所代表方法。那麼事件event關鍵字就是c#提供給我們的一個語法糖。他並沒有任何新的東西,只是減少了一些代碼。所以,事件是一種特殊的委托,其特征有:
1 和一個(基底)委托類型合作,當聲明了一個具有該基底委托類型的事件之後,用戶在外部就可以為這個事件掛接方法,其方法類型必須和基底委托類型相同
2 可以將事件視為委托的方法鏈,使用event關鍵字時,該方法鏈是私有的,而使用委托類型名作為關鍵字時,該方法鏈是公共的
3 事件雖然是私有的,但編譯器自動為我們建立了兩個隱藏方法,並重載了+=和-=,使得我們可以不編寫任何額外的代碼,利用+=和-=操作事件上的方法,從而控制當事件觸發時執行什麼方法
public class Program { public static void Main() { //創建了一個新的訂閱者 var c = new Car("Mycar", 0, 100); //還記得委托麼,如果methodList是一個CarEngineHandler類型則可以這樣寫 //現在methodList是一個事件類型,這樣寫會報錯 //事件只能出現在+=或者-=的左方,不能出現在等於號的左方 //故此時用戶不能隨意為其賦值,保證了安全 c.methodList = OnCarEvent1; //這是正確的 c.methodList += OnCarEvent1; //不能在外部這樣調用一個事件 //c.methodList.Invoke() } public static void OnCarEvent1(string msg) { Console.WriteLine("***** message from car *****"); Console.WriteLine("=> " + msg); Console.WriteLine("****************************"); } public static void OnCarEvent2(string msg) { Console.WriteLine("=> " + msg.ToUpper()); } } public class Car { public string name { get; set; } public int currentSpeed { get; set; } public int MaxSpeed { get; set; } private bool isDead { get; set; } public delegate void CarEngineHandler(string message); public event CarEngineHandler methodList; public Car(string name, int currentSpeed, int MaxSpeed) { this.name = name; this.currentSpeed = currentSpeed; this.MaxSpeed = MaxSpeed; this.isDead = false; } public void Accel(int delta) { //死亡時執行訂閱列表中的方法 if (isDead) { if (methodList != null) //在內部調用事件和調用委托沒區別 methodList("Sorry, car is broken"); } else { currentSpeed += delta; if (currentSpeed >= MaxSpeed) isDead = true; else Console.WriteLine("Current speed: " + currentSpeed); } } }
事件的代碼規范
微軟為事件設置了代碼規范,和委托相同,這些規范主要有:
1 委托名必須以Handler結尾
2 委托必須有且僅有兩個參數,第一個為object類型的sender,第二個則是一個集成了EventArgs類型的自定義類類型,名字可以自定
那麼我們使用代碼規范來重新寫一下上面的例子:
public class Program { public static void Main() { //創建了一個新的訂閱者 var c = new Car("Mycar", 0, 100); c.methodList += OnCarEvent1; for (int i = 0; i < 10; i++) { c.Accel(20); } Console.ReadKey(); } public static void OnCarEvent1(object sender, CarEventArgs e) { //現在我們知道是誰觸發了事件 Console.WriteLine("***** message from car: " + sender.ToString() + " *****"); Console.WriteLine("=> " + e.message); Console.WriteLine("****************************"); } } public class Car { public string name { get; set; } public int currentSpeed { get; set; } public int MaxSpeed { get; set; } private bool isDead { get; set; } //標准化的委托定義 //sender: 當發生事件時,告知訂閱者是誰觸發了事件 //e: 當發生事件時傳遞的信息(可以自定義一個類繼承EventArgs類,故可以傳遞任意類型的信息) public delegate void CarEngineHandler(object sender, CarEventArgs e); public event CarEngineHandler methodList; public Car(string name, int currentSpeed, int MaxSpeed) { this.name = name; this.currentSpeed = currentSpeed; this.MaxSpeed = MaxSpeed; this.isDead = false; } public void Accel(int delta) { //死亡時執行訂閱列表中的方法 if (isDead) { if (methodList != null) //在內部調用事件和調用委托沒區別 methodList(name, new CarEventArgs { message = "Sorry, this car is dead." } ); } else { currentSpeed += delta; if (currentSpeed >= MaxSpeed) isDead = true; else Console.WriteLine("Current speed: " + currentSpeed); } } } //自定義事件發生時發送的信息格式 public class CarEventArgs : EventArgs { public string message { get; set; } }
sender和EventArgs
相信很多人在使用.net進行控件的拖拽時,如果拖拽一個按鈕,然後雙擊它,就會發現代碼多了幾行(省略了無關的):
this.button1.Click += new System.EventHandler(this.button1_Click);
private void button1_Click(object sender, EventArgs e) { }
但可能不是所有人都明白那兩個參數是做什麼的,反正我在這個方法裡寫代碼,然後點擊那個按鈕,編譯器就會執行到這裡面的代碼。其實這正是符合微軟命名規范的一個事件的例子。
1. 在初始化時,為這個對象的Click屬性(注意該屬性的類型是一個事件,他的基底委托是EventHandler類型的)綁定了button1_Click方法。此時如果這個事件被調用,那麼就會執行button1_Click方法的代碼。
EventHandler 是C#控件中最常見的委托,它沒有返回類型:
public delegate void EventHandler (Object sender, EventArgs e)
我們注意到,這個委托正好和button1_Click方法簽名相同,也就是說,我們可以將button1_Click方法加入到委托的方法鏈中。而在這裡,方法鏈就是事件類型變量Click。
2. 當單擊按鈕時,經過一系列的消息輪詢最終系統捕獲到了你的單擊動作,再經過一系列處理,最後到了Control.OnClick(System.EventArgs e)方法
3. 在這個方法裡調用事件
protected virtual void OnClick(EventArgs e) { //從Events委托集合中取出名為EventClick的委托 EventHandler handler = (EventHandler)base.Events[EventClick]; //如果不為空則執行這個委托上的方法,也就是button1_Click if (handler != null) { //this就是button1 handler(this, e); } }
大略的整個過程就是:
1. 你的點擊動作被windows捕獲,windows把這個動作(this.button1.Click)作為系統消息發送給程序(底層消息輪詢機制)
2. 程序從自己的消息隊列中不斷的取出消息,並在消息循環中尋找對應的處理方式
3. 對於這個消息,其sender(事件的來源)就是這個按鈕,發送的消息全部在e裡面,在某些事件裡,e用處不大,比如在MouseEventArgs的Mouse事件中,可以看到e包括mouse的坐標值等,以供你的程序使用
4. 因為this.button1.Click掛接了方法button1_Click,故執行這裡面的代碼
所以說實際上當單擊按鈕時,其實編譯器已經幫我們做了很多事情,例如委托的建立,事件的掛接和執行,這些都已經被封裝到你根本就不需要知道也可以編寫出來成功執行的代碼的程度了。那麼委托和事件這個話題差不多就結束了(在c#1.0這個層面上)。在更高版本的c#中,委托和事件被包裝的更加簡便易用了,基於泛型的委托action和func的出現,更是(基本上)完全替代了原生的委托delegate。這些精彩的內容當然要等待到介紹c#2和3的時候一塊講述。
參考資料
http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html
http://www.tracefact.net/csharp-programming/delegates-and-events-in-csharp.aspx
http://www.cnblogs.com/zhili/archive/2012/10/29/ButtonClickEvent.html