写在最前
我相信全网关于委托和事件的文章和概述,大家应该已经读过很多篇。但是就我的观察来看,大多数文在讲述这方面概念时,都会用烧开水和狗叫主人的例子来讲述事件怎么工作,这样比喻固然与生活联系紧密,但看多了难免有一些审美疲劳。
所以今天,我打算结合自己的一些工作经历,再来谈谈我个人对委托和事件的理解,希望能带给大家一些不一样的见解。
先谈概念
委托:一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。在实例化委托时,你可以将其实例与任何具有兼容签名和返回类型的方法相关联。你可以通过委托实例调用方法。委托用于将方法作为参数传递给其他方法。事件处理程序就是通过委托调用的方法。
事件:类或对象可以通过事件向其他类或对象通知发生的相关事情。发送(或引发)事件的类称为“发布者”,接收(或处理)事件的类称为“订阅者”。
以上概述来自MSDN官方文档:
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/delegates/
从概念中我们其实已经可以看出,委托主要是对方法的一种引用,而事件则充当了多个类或者对象进行相互通知的桥梁。
如果我这么解释你可以明白的话,那么我们今天的主题就已经明朗了,下面我们就用具体的代码实例来讲述。
再看例子
委托
我们需要先声明一个委托实例,在C#中,显示声明自定义委托采用delegate关键字,声明方式与声明普通方法相同,需要指定访问范围和返回类型,同时包含访问参数。
同时我们针对委托,声明对应的方法,方法的返回值和参数需要与委托保持一致,若不一致则会在委托传递方法时出现编译错误。
委托执行内部传递方法的方式是使用Invoke方法,此处需注意,C#中同时提供了BeginInvoke和EndInvoke的方法对,用于异步执行内部的方法,具体含义和用法可参考我之前的一篇文章:
浅谈.Net异步编程的前世今生----APM篇
下面我们一起来看一下示例:
using System;namespace DelegateAndEvent
{class Program{public delegate void DelegateWithNoParams();public delegate int DelegateSum(int a, int b);static void Main(string[] args){DelegateWithNoParams delegate1 = new DelegateWithNoParams(FunctionWithNoParams);delegate1.Invoke();DelegateSum delegate2 = new DelegateSum(FunctionSum);int c = delegate2.Invoke(10, 20);Console.WriteLine("带返回值和参数的方法,结果为:" + c);Console.Read();}public static void FunctionWithNoParams(){Console.WriteLine("无返回值无参数的方法");}public static int FunctionSum(int a, int b){int c = a + b;return c;}}
}
在此示例中,我们分别定义了一个无参数无返回值的委托和一个包含2个参数并返回int类型的委托,分别用于执行两种对应的方法。在两个委托执行对应的Invoke方法之后,会产生以下的结果:
结果和我们预期一致,程序同步顺序地执行了两个委托并打印出相应的结果。但是看到这里也许你会有一个疑问,既然委托执行时的结果与直接调用方法一致,那么我们为什么还需要使用委托来执行方法呢?
这时我们就要回到最初的定义:委托用于将方法作为参数传递给其他方法。
由于实例化的委托是一个对象,因此可以作为参数传递或分配给一个属性。这允许方法接受委托作为参数并在稍后调用委托。这被称为异步回调,是在长进程完成时通知调用方的常用方法。当以这种方式使用委托时,使用委托的代码不需要知道要使用的实现方法。功能类似于封装接口提供的功能。
我们一起使用一个比较直观的例子来验证:
using System;namespace ConsoleApp1
{class Program{public delegate void Del(string message);static void Main(string[] args){Del handler = new Del(DelegateMethod);MethodWithCallback(1,2,handler);Console.Read();}public static void DelegateMethod(string message){Console.WriteLine(message);}public static void MethodWithCallback(int param1, int param2, Del callback){callback(string.Format("当前的值为:{0}", (param1 + param2)));}}
}
在这段代码中,我们声明了一个无返回值委托Del,用于接收传入的消息,并且该委托指向了一个调用控制台的方法DelegateMethod。而后续我们调用MethodWithCallback方法时,无需调用控制台相关方法,而是直接将Del委托的实例作为参数传入,就实现DelegateMethod方法的调用。这个实例就是我们上述提到的异步回调和委托对方法的引用。
运行结果如下:
此处我使用了JetBrains出品的IDE Rider,因此截图界面会与VS有所不同,喜欢轻量级IDE的同学可以试试这款,有30天的免费试用期,地址:Rider: The Cross-Platform .NET IDE from JetBrains,此处不再过多讲解。
根据我们上述的实例讲解,大家对委托的作用及使用场景应该有了初步的理解,但是我们仔细想一想,上述的场景似乎少了些什么,为什么我们的委托始终只能指向一个方法进行调用呢?
这里就要引出我们接下来的概念:多播委托。
事实上,委托是可以调用多个方法的,这种方式就叫做多播委托,在C#中,我们可以使用+=的运算符,将其他委托附加到当前委托之后,就可以实现多播委托,相关示例如下:
using System;namespace ConsoleApp1
{class Program{public delegate void Del();static void Main(string[] args){Del handler = new Del(DelegateMethod1);Del handlerNew = new Del(DelegateMethod2);handler += handlerNew;handler.Invoke();Console.Read();}public static void DelegateMethod1(){Console.WriteLine("天才第一步!");}public static void DelegateMethod2(){Console.WriteLine("天才第二步!");}}
}
在这个示例中,我们重新编写了一个方法叫DelegateMethod2,同时我们又声明了一个新的委托对象handlerNew指向该方法。接着我们使用+=的方式将handlerNew添加至handler并执行该委托,得到的结果如下:
如我们先前所料,多播委托把多个方法依次进行了执行。此时如果某个方法发生异常,则不会调用列表中的后续方法,如果委托具有返回值和/或输出参数,它将返回上次调用方法的返回值和参数。与增加方法相对应,若要删除调用列表的方法,则可以使用-=运算符进行操作。
关于委托的理解与常用方式,我们就讲解到这里,事实上,多播委托常用于事件处理中,由此可见,事件与委托有着千丝万缕的联系,下面我们就拉开事件的序幕。
事件
如前文讲解时所说,事件是一种通知行为,因此要分为事件发布者和事件订阅者。而且在.Net中,事件基于EventHandler委托和EventArgs基类的,因此我们在声明事件时,需要先定义一个委托类型,然后使用event关键字进行事件的定义。
相关的示例如下:
using System;namespace ConsoleApp1
{public class PublishEvent{public delegate void NoticeHandler(string message);public event NoticeHandler NoticeEvent;public void Works(){//触发事件OnNoticed();}protected virtual void OnNoticed(){if (NoticeEvent != null){//传递事件及参数NoticeEvent("Notice发布的报警信息!");}}}public class SubscribEvent{public SubscribEvent(PublishEvent pub){//订阅事件pub.NoticeEvent += PrintResult;}/// <summary>/// 订阅事件后的响应函数/// </summary>/// <param name="message"></param>void PrintResult(string message){Console.WriteLine(string.Format("已收到{0}!采取措施!",message));}}class Program{static void Main(string[] args){PublishEvent publish = new PublishEvent();SubscribEvent subscrib = new SubscribEvent(publish);//触发事件publish.Works();Console.Read();}}
}
从事例中我们可以看出,我们分别定义了发布者和订阅者相关的类。
在发布者中,我们需要声明一个委托NoticeHandler,然后定义一个此类型的事件NoticeEvent。在定义对象之后,我们需要对事件进行执行,因此有了OnNoticed方法,此方法只用于事件本身的执行。那么什么时候才能执行事件呢?于是我们又有了触发该事件的方法Works,当Works方法被调用时,就会触发NoticeEvent事件。
而在订阅者中,我们需要对NoticeEvent事件进行订阅,因此我们需要发布者的对象PublishEvent,同时需要对它的事件进行订阅。正如我们前文所说,订阅使用+=的方式,与多播委托的使用是一致的,而+=后的对象正是我们需要响应后续处理的方法PrintResult。当事件被触发时,订阅者会接收到该事件,并自动执行响应函数PrintResult。
执行结果如下图所示:
从执行结果中我们可以看出,在事件被触发后,订阅者成功接收到了发布者发布的事件内容,并进行自动响应,而我们在此过程中从未显式调用订阅者的任何方法,这也是事件模型的本质意义:从发布到订阅。
在微软官方文档中提到,事件是一种特殊的多播委托,只能从声明它的类中进行调用。客户端代码通过提供对应在引发事件时调用的方法的引用来订阅事件。这些方法通过事件访问器添加到委托的调用列表中,这也是我们可以使用+=去订阅事件的原因所在,而取消事件则和多播委托一致,使用-=的方式。
关于事件的使用场景还有一种与多线程通知相关的典型用法,具体含义和用法可参考我之前的另一篇文章:
浅谈.Net异步编程的前世今生----EAP篇
最后总结
本文我们讲解了委托和事件之间的关系,以及它们的使用场景。在我个人的工作经历中,曾经有3年左右的时间从事C/S相关的开发工作,其中包含了大量多线程、委托和事件的使用场景。主要用于在开发WinForm程序时,不同窗体(包含父子窗体)之间进行相互通信,其中都是基于事件的发布和订阅作为实现的。而委托的使用场景则更多,很多C#方法在使用时都会传入一个Action或者Func委托作为参数,而这些参数同时又支持Lambda表达式,这就又引出了匿名函数的概念,由于篇幅所限,此处不再进行讲解,大家可以自行搜索资料进行了解和学习。
除了具体的技术点之外,在我们的设计模式中也有事件的使用身影,最典型的莫过于观察者模式。关于观察者模式,网上众说纷纭,也有很多资料会将它与发布-订阅模式混为一谈。而实际上这两种模式并不完全是同一种概念和实现方式,那么下一次我们将会从设计模式着手,谈一谈观察者模式和发布-订阅模式的异同,敬请期待!
您的点赞和在看是我创作的最大动力,感谢支持
公众号:wacky的碎碎念
知乎:wacky