举一个简单的例子,.NET中经常使用的控件Button,当我们把Button控件拖放到界面,然后双击界面的Button我们发现程序中自动生成了一个响应Button的事件方法,然后我们给事件方法添加Code之后,当我们点击该Button就响应该方法了,但我们没有看到代码中有任何的委托和事件之类的定义,其实这些.NET都已经做好了。我们可以查看如下文件。
其中,EventHandler就是一个委托,类似于一种代理类型,可以认为它是一个“类”,是所有返回类型为void的委托类。事件其实是一种特殊的函数,它的两个参数分别是object sender和EventArgs e,第一个参数表示引发事件的控件,或者说它表示点击的那个按钮,第二个参数表示事件发生时所使用的资源,可以理解为一个封装的参数的集合。下面从委托开始解析.NET事件吧。
前面说了很多理论,对事件进行了初步的解析,从解析中可以看出,事件其实采用了委托。对于.NET程序猿来说,委托就像是一道槛儿,过了这个槛的人,觉得真是太容易了,而没有过去的人每次见到委托和事件就觉得心里憋得慌,混身不自在。那委托究竟如何使用呢,让我们来看个示例。
现在我们要在窗体Label上打印字符串,而且使用高级点的方法--委托来获取需要打印的字符串,并在其它类中实现打印。
首先我们要声明一个自定义的委托,声明时使用关键字delegate。该委托使用了两个参数object和EventResource,object是要打印字符串的控件,即为上面所说的Label控件;EventResource是资源类,从该类中获取要打印的字符串。这里声明了一个名为PrintStr的委托类,其实委托类似于一种中介,它将方法的发出者和方法的接收者联系到一起,方法的发出者对接收者一无所知,是通过委托作为一个中介,把处理方法注册到发出者当中,这样就实现了由方法发送者->委托->方法接收者的过程了。
/// <summary> /// 自定义的委托 /// </summary> /// <param name="obj"></param> /// <param name="er"></param> /// <returns></returns> public delegate string PrintStr(object obj, EventResource er); /// <summary> /// 资源类,自定义委托所使用的数据 /// </summary> public class EventResource : EventArgs { public string str = "This is a EventResource"; //类的一个资源属性 /// <summary> /// 自定义方法,返回传入的字符串 /// </summary> /// <param name="strParam">返回的字符串</param> /// <returns></returns> public string ReturnStr(string strParam) { return strParam; } }
委托类声明完成后,现在来编写具体的实现打印的方法,如下我们在类中声明了两个方法分别为PrintStr和PrintStr1具体使用哪个方法需要在委托中指定。
Note:需要使用到委托的方法,它的定义必须符合委托类型签名匹配,即:函数的返回类型和所需要的参数类型及个数必须严格和自定义的委托类相同。
/// <summary> /// 打印字符串类 /// </summary> public class PrintString { /// <summary> /// 打印字符串函数1 /// </summary> /// <param name="obj"></param> /// <param name="er"></param> /// <returns></returns> public string Print(object obj, EventResource er) { Label lbl = (Label)obj; lbl.Text = "Print " + er.str + er.ReturnStr(" Yes!"); return "Print " + er.str + er.ReturnStr(" Yes!"); } /// <summary> /// 打印字符串函数2 /// </summary> /// <param name="obj"></param> /// <param name="er"></param> /// <returns></returns> public string Print1(object obj, EventResource er) { return "\nPrint1 " + er.str + er.ReturnStr(" Yes!"); } }
使用委托的过程,首先要声明委托和委托所使用的参数,然后为委托的构造函数赋值,将具体方法的接收者的名字传给我们创建的委托,最后执行委托即可。
private void button1_Click(object sender, EventArgs e) { Delegate.PrintStr de; //声明委托类 PrintString ps = new PrintString(); //声明并创建PrintString类 EventResource er = new EventResource(); //声明并创建使用的资源类 de = new PrintStr(ps.Print); //把需要执行的方法传给委托 de(label1, er); //委托执行方法,在Label1控件上打印字符串 }
运行结果:
上例我们从创建委托到最后的执行委托,经历了一个完整的委托过程。从实例中可以看出委托完全可以看做是一个类,它定义了方法的类型,使得可以将方法当做另一个方法的参数来传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。
我们返回到发送器和接受器的角度讨论事件,在UI编程中,鼠标单击或键盘按键,发送器就是.NET的CLR,注意事件发送器并不知道接收器是谁,这符合面向对象的原则,而且某个事件接收器有个方法处理该事件,这个时候就要委托,正如上节中所说到的,委托实现了发送器和接收器的关联。
前面使用的委托都只包含一个方法调用。调用一个委托就调用一个方法调用。如果要通过一个委托调用多个方法,那就需要使用委托的多播特性。如果调用多播委托,就可以按委托添加次序连续调用多个方法。为此,委托的签名就必须返回void;否则,就只能得到委托调用的最后一个方法的结果,接下来看看多播实现。
/// <summary> /// 自定义的委托 /// </summary> /// <param name="obj"></param> /// <param name="er"></param> /// <returns></returns> public delegate string PrintStr(object obj, EventResource er); /// <summary> /// 打印字符串类,封装了两个打印方法 /// </summary> public class PrintString { /// <summary> /// 打印字符串函数1 /// </summary> /// <param name="obj"></param> /// <param name="er"></param> /// <returns></returns> public string PrintStr(object obj, EventResource er) { Label lbl = (Label)obj; lbl.Text = "\nPrintStr " + er.str + er.ReturnStr(" Yes!"); return "PrintStr " + er.str + er.ReturnStr(" Yes!"); } /// <summary> /// 打印字符串函数2 /// </summary> /// <param name="obj"></param> /// <param name="er"></param> /// <returns></returns> public string PrintStr1(object obj, EventResource er) { Label lbl = (Label)obj; lbl.Text =lbl.Text+ "\nPrintStr1 " + er.str + er.ReturnStr(" Yes!"); return "\nPrintStr1 " + er.str + er.ReturnStr(" Yes!"); } } /// <summary> /// 执行委托的事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void button1_Click(object sender, EventArgs e) { Delegate.PrintStr de=null; //声明委托类 PrintString ps = new PrintString(); //声明并创建PrintString类 PrintString ps1 = new PrintString(); EventResource er = new EventResource(); //声明并创建使用的资源类 de += ps.PrintStr; //把需要执行的方法传给委托 de += ps.PrintStr1; de(label1,er); //委托执行方法,在Label1控件上打印字符串 }
上面实现了多播委托,就是通过“+”把方法调用绑定到委托变量中,如果我们用“-”就可以移除绑定到委托变量方法了,其实很简单。
上面一直都是在说委托,似乎和我们要讨论的事件没有什么关系,别着急继续往下看。另外也有一个疑问一直没有解答,那就是事件。事件它是如何形成的呢,接下来看下文章的重点,事件的处理机制!
事件是特殊类型的多路广播委托,仅可从声明它们的类或结构(发行者类)中调用。 如果其他类或结构订阅了该事件,则当发行者类引发该事件时,会调用其事件处理程序方法。
现在来看看下面的示例,使用事件对委托进行封装。
namespace ConsoleApplication2 { public delegate void ConsolePrint(string str); //声明打印方法的委托 public class PrintManage { public static event ConsolePrint cp; //声明事件cp //添加方法执行自定义的事件 public static void PrintManagement(string str) { cp(str); } } public static class Program { /// <summary> /// 打印英文 /// </summary> /// <param name="str"></param> public static void PrintEnglish(string str) { Console.WriteLine(str+"This is English!"); } /// <summary> /// 打印中文 /// </summary> /// <param name="str"></param> public static void PrintChinese(string str) { Console.WriteLine(str+"这是汉语!" ); } static void Main(string[] args) { PrintManage.cp += Program.PrintEnglish; //在事件中添加委托 PrintManage.cp += Program.PrintChinese; //执行事件 PrintManage.PrintManagement("Program"); Console.ReadKey(); } } }
运行结果:
接下来使用反编译的方法,把上面的程序反编译,会发现在PrintManage类中增加了私有的委托属性,如下图:
难道是我们的代码编写错了吗,显然不是,通过查阅资料发现其实事件和委托关系就像是属性和字段的关系,为了刚好的实现OOP的编程原则(对扩展开发修改关闭),事件对委托进行了封装。下次相对事件进行扩展的时候只需要添加新的方法,然后添加到多播委托事件中,这样做很好的符合OOP的编程原则。
上文已经从事件的处理和产生角度讨论了事件,通过讨论我们不难看出事件其实是一种特殊的函数,微软官方的描述是特殊类型的多路广播委托,也就是说通过绑定多播委托,能够使我们写的函数在编译完成后成为事件,从而自动触发。
本文中首先提出了事件委托的概念,然后通过自定义委托的示例来说明了委托的使用方法,当然委托的用处不之是上面所说的一种,还有更多。最后使用两个示例来说明了多播委托和事件的处理机制。想要对事件了解更多,请参阅MSDN:事件(C#编程指南)
相信通过上面的讨论已经能够清晰的了解.NET事件和委托的处理机制,事件是一种特殊类型的多播委托,通过使用委托保证了事件的可扩展性,很好的符合了OOP的编程原则,希望这篇文章能给.NET的初学者带来帮助。