C#事件
C#中的事件是類發送通知或信息到其它類的一種溝通機制。當有事情發生的時候,事件做為一種專門的委托,我們可以用於通知其它的類。事件是很多應用必不可少的一部分,是解耦和創建靈活的且可擴展的應用程序。
在這篇文章中,我們將學習事件是什么,如何使用事件。
讓我們開始吧!
什么是委托?
正如我們在前言中所提到的,C#事件是一個專門的委托類型。
但是,准確來講,這意味着什么呢?
為了能恰當地解釋事件,我們先簡要地說一下委托是什么。委托是對函數的引用,如果我們調用委托,實際是調用的委托引用的函數。委托擁有一個函數或一組函數的所有必要信息,包括簽名和返回值類型。我們可以將委托做為一個參數傳遞給函數,或將委托包含在結構或類中。靜態設計時,當我們不知道具體哪個函數會被調用時,我們可以使用委托。
聽起來可能比較復雜,但其實不難。我們用一個簡單的示例看看如何使用委托。
我們用delegate關鍵詞來定義委托:
然后,定義一個委托要引用的函數:
1 void WriteText(string text) 2 { 3 Console.WriteLine($"Text:{text}"); 4 }
委托和函數有相同的簽名和返回類型,我們可以將委托和函數關聯起來:
還有一個簡短的版本:
SendMessage delegate2 = WriteText;
還可以使用匿名函數:
SendMessage delegate3 = delegate(string text) { Console.WriteLine($"Text: {text}"); };
或者使用lambda表達式更加簡潔:
SendMessage delegate4 = text => { Console.WriteLine($"Text: {text}"); };
最后這個示例可能是我們經常看到的更加現代的寫法。
現在,我們可以在方法體中使用委托,或者在結構或類中創建委托。
什么是事件?
事件是委托的一個子集,為了滿足“廣播/訂閱”模式的需求而生。為觸發事件(To raise an event),需要一個事件發布者,為接收和處理事件,需要一個訂閱者或多個訂閱者。
這些通常是由發布者類和訂閱者類來進行實現。
所以,我們為什么要在C#中使用事件呢?
我們使用事件:
1、解耦我們的應用程序,或者說是松耦合;
2、用於對象之間聯系的運行機制;
3、在不改變已有代碼的情況下,提供一種簡單而有效的方式去擴展應用程序;
在沒有破壞已有代碼的情況下,松耦合程序容易擴展並達到我們想要做的。
我們創建不使用event的應用程序,看下事件是如何工作的,並由此探討一些問題。
我們的程序是一個食品預購服務,所以我們創建了Order類:
1 public class Order 2 { 3 public string Item { get; set; } 4 public string Ingredients { get; set; } 5 }
一個簡單的類包含食品類目名稱和成分。
然后,有一個處理預訂的真正服務:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace Order 8 { 9 class FoodOrderingService 10 { 11 static void Main(string[] args) 12 { 13 14 } 15 16 public void PrepareOrder(Order order) 17 { 18 Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item); 19 Thread.Sleep(4000); 20 21 AppService _appService = new AppService(); 22 _appService.SendAppNotification(); 23 } 24 } 25 public class AppService 26 { 27 public void SendAppNotification() 28 { 29 Console.WriteLine("AppService:your food is prepared!"); 30 } 31 } 32 33 public class Order 34 { 35 public string Item { get; set; } 36 public string Ingredients{get;set;} 37 } 38 39 40 }
正如我們看到的,我們拿到了需要准備的菜單,模擬4秒鍾的等待時間以進行准備,然后我們發送了一條通知到用戶程序:預訂在准備。當然,這僅僅是個測試所以示例相當簡單,我們用console模擬通知消息。在實際的應用中會包含很多步驟。
最后,我們調用服務來購買一個帶很多奶酪的pizza:
1 static void Main(string[] args) 2 { 3 var order = new Order { Item = "Pizza with extra cheese" }; 4 5 var orderingService = new FoodOrderingService(); 6 orderingService.PrepareOrder(order); 7 8 Console.ReadKey(); 9 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace Order 8 { 9 class FoodOrderingService 10 { 11 static void Main(string[] args) 12 { 13 var order = new Order { Item = "Pizza with extra cheese"}; 14 var orderingService = new FoodOrderingService(); 15 orderingService.PrepareOrder(order); 16 17 Console.ReadKey(); 18 } 19 20 public void PrepareOrder(Order order) 21 { 22 Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item); 23 Thread.Sleep(4000); 24 25 AppService _appService = new AppService(); 26 _appService.SendAppNotification(); 27 } 28 } 29 public class AppService 30 { 31 public void SendAppNotification() 32 { 33 Console.WriteLine("AppService:your food is prepared!"); 34 } 35 } 36 37 public class Order 38 { 39 public string Item { get; set; } 40 public string Ingredients{get;set;} 41 } 42 43 44 }
我們運行程序會看到order已經運行,過了4秒鍾后顯示“AppService:your food is prepared!”。就程序而言,運行很簡單,匆匆而過,沒有什么問題。
但是,我們決定擴展程序用email通知用戶他們的訂餐准備好了。
為達到上面的目的,我們擴展service類:
1 public class FoodOrderingService 2 { 3 ... 4 public void PrepareOrder(Order order) 5 { 6 Console.WriteLine($"Preparing your order '{order.Item}', please wait..."); 7 Thread.Sleep(4000); 8 9 _appService.SendAppNotification(); 10 _mailService.SendEmailNotification(); 11 } 12 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace Order 8 { 9 class FoodOrderingService 10 { 11 static void Main(string[] args) 12 { 13 var order = new Order { Item = "Pizza with extra cheese"}; 14 var orderingService = new FoodOrderingService(); 15 orderingService.PrepareOrder(order); 16 17 Console.ReadKey(); 18 } 19 20 public void PrepareOrder(Order order) 21 { 22 Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item); 23 Thread.Sleep(4000); 24 25 AppService _appService = new AppService(); 26 _appService.SendAppNotification(); 27 28 MailService _mailService = new MailService(); 29 _mailService.SendEmailNotification(); 30 } 31 } 32 public class AppService 33 { 34 public void SendAppNotification() 35 { 36 Console.WriteLine("AppService:your food is prepared!"); 37 } 38 } 39 public class MailService 40 { 41 public void SendEmailNotification() 42 { 43 Console.WriteLine("MailService:your food is prepared."); 44 } 45 } 46 47 public class Order 48 { 49 public string Item { get; set; } 50 public string Ingredients{get;set;} 51 } 52 53 54 }
如上所示,隨着我們的需求改變,通過添加mailService類我們也已經更改了用餐通知服務。這很容易給我們的程序引入bug,甚至是如果我們寫了個單元測試,我們可能要重新梳理並修改代碼。同理,我們在FoodOrderingService類中同時引入appservice 和 mailservice函數來發送通知消息,由此創建了一個緊密相聯的耦合程序。
因此,我們已經看到這種實現方法不是我們期望的!
我們嘗試使用event事件提升一下這個示例,這是引入發布-訂閱模式的完美場合。
首先明確的是,我們要用委托創建一個事件的示例,目的是了解背后發生了什么。稍后,我們會學習更簡單的方法做同樣的事情。
事件的實施
好,現在讓我們看一下如何運用事件並用示例觸發事件。
我們需要做:
1、定義一個委托;
2、定義一個依賴委托的事件;
3、觸發事件;
以聲明一個委托來開始示例:
1 public class FoodOrderingService 2 { 3 // define a delegate 4 public delegate void FoodPreparedEventHandler(object source, EventArgs args); 5 6 public void PrepareOrder(Order order) 7 { 8 Console.WriteLine($"Preparing your order '{order.Item}', please wait..."); 9 Thread.Sleep(4000); 10 } 11 }
委托FoodPreparedEventHandler返回void,委托通常有2個參數,第一個參數是事件源(表示觸發事件的那個組件 如(button/label/listview...),比如說你單擊button,那么sender就是button),更確切地說是將要發布事件的那個類;第二個參數EventArgs(它用來輔助你處理事件,比如說你用鼠標點擊窗體,那么EventArgs是會包含點擊的位置等等),是與事件相關的任何其它數據。
通常來說,我們會給委托起一個描述性的名字,比如“FoodPrepared”,然后在名字末尾添加上“EventHandler”。無論委托名稱怎么變化,人們都能很容易了解這是個委托。
現在,讓我們創建一個事件:
1 public class FoodOrderingService 2 { 3 // define a delegage 4 public delegate void FoodPreparedEventHandler(object source, EventArgs args); 5 // declare the event 6 public event FoodPreparedEventHandler FoodPrepared; 7 8 public void PrepareOrder(Order order) 9 { 10 Console.WriteLine($"Preparing your order '{order.Item}', please wait..."); 11 Thread.Sleep(4000); 12 } 13 }
事件是FoodPreparedEventHandler
類型,因為我們定義的是一旦操作完成就會觸發的事件,所以我們給它起了個過去時的名字---FoodPrepared
。完全可能有一種表明目前正在發生的事件,比如,我們會定義另一個事件,顯示食物正在准備中。那樣的話,我們會將事件叫做FoodPreparation
。
現在,我們已經定義了委托和事件,為了觸發事件,我們應創建一個方法函數:
1 public class FoodOrderingService 2 { 3 public delegate void FoodPreparedEventHandler(object source, EventArgs args); 4 public event FoodPreparedEventHandler FoodPrepared; 5 6 public void PrepareOrder(Order order) 7 { 8 Console.WriteLine($"Preparing your order '{order.Item}', please wait..."); 9 Thread.Sleep(4000); 10 11 OnFoodPrepared(); 12 } 13 14 protected virtual void OnFoodPrepared() 15 { 16 if (FoodPrepared != null) 17 FoodPrepared(this, null); 18 } 19 }
按照慣例,方法函數的修飾符應該是protected virtual void,名稱前綴加“On”。在函數體內部,我們檢查是否有訂閱者(FoodPrepared != null),如果有訂閱者,我們就調用事件,將this做為參數傳遞,this是當前類;null做為事件參數。
一旦我們的order完成了,我們會在PrepareOrder
函數中調用關聯的方法,因此我們開始准備創建訂閱者。
如何訂閱事件
我們首先創建AppService
類:
1 public class AppService 2 { 3 public void OnFoodPrepared(object source, EventArgs eventArgs) 4 { 5 Console.WriteLine("AppService: your food is prepared."); 6 } 7 }
我們已經創建了一個叫AppService類,AppService類中有一個方法的簽名與事件相同。
現在我們實例化AppService類,並訂閱到事件。
1 static void Main(string[] args) 2 { 3 var order = new Order { Item = "Pizza with extra cheese" }; 4 5 var orderingService = new FoodOrderingService(); 6 var appService = new AppService(); 7 8 orderingService.FoodPrepared += appService.OnFoodPrepared; 9 10 orderingService.PrepareOrder(order); 11 12 Console.ReadKey(); 13 }
用 += 運算符可以訂閱到事件,在上面的示例中我們訂閱到FoodPrepared事件中,用AppService
類中的OnFoodPrepared
函數來處理事件。
如果我們運行程序,會看到AppService類處理了事件並輸出如下:
以上結果正是我們想要的,我們沒有將AppService加入到FoodOrderingService類中,也沒改變
FoodOrderingService類
的實現方式。
我們可以很容易地用另一個訂閱者進一步擴展此程序,我們創建另一個類叫MailService:
1 public class MailService 2 { 3 public void OnFoodPrepared(object source, EventArgs eventArgs) 4 { 5 Console.WriteLine("MailService: your food is prepared."); 6 } 7 }
MailService類與AppService類幾乎是一樣的,我們已經改變了輸出,所以可以想象出在控制台會發生什么。
現在我們也將MailService訂閱到事件:
如果我們運行程序,會看到訂閱者已經處理了事件:

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace Order 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 var order = new Order { Item = "Pizza with extra cheese" }; 14 var orderingService = new FoodOrderingService(); 15 var appService = new AppService(); 16 var mailService = new MailService(); 17 18 orderingService.FoodPrepared += appService.OnFoodPrepared; 19 orderingService.FoodPrepared += mailService.OnFoodPrepared; 20 21 orderingService.PrepareOrder(order); 22 Console.ReadKey(); 23 } 24 } 25 26 class FoodOrderingService 27 { 28 //定義委托 29 public delegate void FoodPreparedEventHandler(object source, EventArgs args); 30 //聲明事件 31 public event FoodPreparedEventHandler FoodPrepared; 32 33 public void PrepareOrder(Order order) 34 { 35 36 Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item); 37 Thread.Sleep(4000); 38 39 OnFoodPrepared(); 40 } 41 //觸發事件的函數方法 42 protected virtual void OnFoodPrepared() 43 { 44 if (FoodPrepared != null) 45 { 46 FoodPrepared(this,null); 47 } 48 } 49 } 50 public class AppService 51 { 52 public void OnFoodPrepared(object source ,EventArgs eventArgs) 53 { 54 Console.WriteLine("AppService:your food is prepared!"); 55 } 56 } 57 public class MailService 58 { 59 public void OnFoodPrepared(object sorece , EventArgs eventArgs) 60 { 61 Console.WriteLine("MailService:your food is prepared."); 62 } 63 } 64 65 public class Order 66 { 67 public string Item { get; set; } 68 public string Ingredients{get;set;} 69 } 70 71 72 }
以上就是我們想要的結果,再說一次,我們沒有改變FoodOrderingService類中的任何代碼!
我們可以像這樣無限地擴展我們的程序,我們也可以將FoodOrderingService
類移到其它類庫中或我們想要的地方。
擴展EventArgs
像我們前面提到的那樣,我們用EventArgs發送事件數據。我們能創建自EventArgs繼承的自定義事件參數:FoodPreparedEventArgs,用於發送數據到訂閱者。
1 public class FoodPreparedEventArgs : EventArgs 2 { 3 public Order Order { get; set; } 4 }
然后,我們修改FoodOrderingService
:
1 public class FoodOrderingService 2 { 3 public delegate void FoodPreparedEventHandler(object source, FoodPreparedEventArgs args); 4 public event FoodPreparedEventHandler FoodPrepared; 5 6 public void PrepareOrder(Order order) 7 { 8 Console.WriteLine($"Preparing your order '{order.Item}', please wait..."); 9 Thread.Sleep(4000); 10 11 OnFoodPrepared(order); 12 } 13 14 protected virtual void OnFoodPrepared(Order order) 15 { 16 if (FoodPrepared != null) 17 FoodPrepared(this, new FoodPreparedEventArgs { Order = order }); 18 } 19 }
現在我們正發送order數據(OnFoodPrepared(order);)到訂閱者。我們剛才用FoodPreparedEventArgs
代替了EventArgs並將order信息傳遞給了訂閱者。
所以,讓我們更改訂閱者處理數據的方式。首先更改AppService
類:
1 public class AppService 2 { 3 public void OnFoodPrepared(object source, FoodPreparedEventArgs eventArgs) 4 { 5 Console.WriteLine($"AppService: your food '{eventArgs.Order.Item}' is prepared."); 6 } 7 }
我們現在用FoodPreparedEventArgs
代替了泛型EventArgs
,你看到過我們可以通過eventArgs
參數處理order類目名稱。
我們也可以修改MailService類:
1 public class MailService 2 { 3 public void OnFoodPrepared(object source, FoodPreparedEventArgs eventArgs) 4 { 5 Console.WriteLine($"MailService: your food '{eventArgs.Order.Item}' is prepared."); 6 } 7 }
輸出如下
我們的services類已經讀取了order信息,並將它打印在控制台上。完整代碼如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace Order 8 { 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 var order = new Order { Item = "Pizza with extra cheese" }; 14 var orderingService = new FoodOrderingService(); 15 var appService = new AppService(); 16 var mailService = new MailService(); 17 18 orderingService.FoodPrepared += appService.OnFoodPrepared; 19 orderingService.FoodPrepared += mailService.OnFoodPrepared; 20 21 orderingService.PrepareOrder(order); 22 Console.ReadKey(); 23 } 24 } 25 26 class FoodOrderingService 27 { 28 //定義委托 29 public delegate void FoodPreparedEventHandler(object source, FoodPreparedEventArgs args); 30 //聲明事件 31 public event FoodPreparedEventHandler FoodPrepared; 32 33 public void PrepareOrder(Order order) 34 { 35 36 Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item); 37 Thread.Sleep(4000); 38 39 OnFoodPrepared(order); 40 } 41 //觸發事件的函數方法 42 protected virtual void OnFoodPrepared(Order order) 43 { 44 if (FoodPrepared != null) 45 { 46 FoodPrepared(this, new FoodPreparedEventArgs { Order = order}); 47 } 48 } 49 } 50 public class AppService 51 { 52 public void OnFoodPrepared(object source, FoodPreparedEventArgs eventArgs) 53 { 54 Console.WriteLine("AppService:your food {0} is prepared!",eventArgs.Order.Item); 55 } 56 } 57 public class MailService 58 { 59 public void OnFoodPrepared(object sorece , FoodPreparedEventArgs eventArgs) 60 { 61 Console.WriteLine("MailService:your food {0} is prepared!", eventArgs.Order.Item); 62 } 63 } 64 65 public class Order 66 { 67 public string Item { get; set; } 68 public string Ingredients{get;set;} 69 } 70 71 public class FoodPreparedEventArgs : EventArgs 72 { 73 public Order Order { get; set; } 74 } 75 }
用EventHandler類簡化項目
我們提升一下項目,方式不是我們每天看到的那種。我們這么做是為了讓我們能夠對事件的底層運行機制有一個清晰的了解。
現在,通過.net用一些適合我們的現代方式修改代碼,在C#中使用event handler。
主要使用EventHandler
和EventHandler<TEventArgs>
創建事件,他們是專門的封裝器,可以簡化事件的創建。
所以,我們通過添加event handler修改我們的FoodOrderingService類:
1 public class FoodOrderingService 2 { 3 public event EventHandler<FoodPreparedEventArgs> FoodPrepared; 4 5 public void PrepareOrder(Order order) 6 { 7 Console.WriteLine($"Preparing your order '{order.Item}', please wait..."); 8 Thread.Sleep(4000); 9 10 OnFoodPrepared(order); 11 } 12 13 protected virtual void OnFoodPrepared(Order order) 14 { 15 FoodPrepared?.Invoke(this, new FoodPreparedEventArgs { Order = order }); 16 } 17 }
現在,用於代替委托和事件的兩種聲明,我們用EventHandler
和FoodPreparedEventArgs
,這樣可以使代碼更簡潔,更有可讀性。這可能是你在其它項目中使用事件時會看到的。
另一個提升是使用了傳統的Invoke()
函數來觸發事件,避免復雜的null檢查,可以清理項目冗余代碼。
訂閱到事件
還剩下一件事情需要討論。到目前我們所見,事件以發布-訂閱模式進行工作,那意味着一旦我們訂閱了事件,只有服務在訂閱就在。
但是有時候生意邏輯就規定了可以訂閱也可以取消。比如,用戶可能有權選擇只接收app通知或只接收郵件通知。
上述情況下,我們能夠訂閱到我們想要訂閱的事件,我們可以使用-=運算符:
1 orderingService.FoodPrepared -= appService.OnFoodPrepared; 2 orderingService.FoodPrepared -= mailService.OnFoodPrepared;
有時,當組件被實例化多次,我們可以多次訂閱到同一個事件。這就是為什么在Dispose函數方法中我們要小心、正確地處理。
總結
這篇文章中,我們已經學習了有關事件的基本概念,學習了事件背后如何工作,以及委托在事件參數中扮演的重要角色。
我們創建了一個簡單的測試程序,來顯示事件如何運行,如何 觸發 ,如何訂閱,取消訂閱。最后一件事情是我們用.NET提供的語法糖來提升項目代碼。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 7 namespace Order 8 { 9 class FoodOrderingService 10 { 11 //定義委托 12 public delegate void FoodPreparedEventHandler(object source, EventArgs args); 13 //聲明事件 14 public event FoodPreparedEventHandler FoodPrepared; 15 16 public void PrepareOrder(Order order) 17 { 18 19 Console.WriteLine("Preparing your order '{0}',please wait ......", order.Item); 20 Thread.Sleep(4000); 21 22 OnFoodPrepared(); 23 } 24 //觸發事件的函數方法 25 protected virtual void OnFoodPrepared() 26 { 27 if (FoodPrepared != null) 28 { 29 FoodPrepared(this,null); 30 } 31 } 32 33 static void Main(string[] args) 34 { 35 var order = new Order { Item = "Pizza with extra cheese"}; 36 var orderingService = new FoodOrderingService(); 37 var appService = new AppService(); 38 var mailService = new MailService(); 39 40 orderingService.FoodPrepared += appService.OnFoodPrepared; 41 orderingService.FoodPrepared += mailService.OnFoodPrepared; 42 43 orderingService.PrepareOrder(order); 44 Console.ReadKey(); 45 } 46 } 47 public class AppService 48 { 49 public void OnFoodPrepared(object source ,EventArgs eventArgs) 50 { 51 Console.WriteLine("AppService:your food is prepared!"); 52 } 53 } 54 public class MailService 55 { 56 public void OnFoodPrepared(object sorece , EventArgs eventArgs) 57 { 58 Console.WriteLine("MailService:your food is prepared."); 59 } 60 } 61 62 public class Order 63 { 64 public string Item { get; set; } 65 public string Ingredients{get;set;} 66 } 67 68