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