委托和事件
委托实现了类型安全的回掉方法,在.NET中回调无处不在,所以委托也无处不在,事件模型建立在委托机制上,本文将完成一次关于委托的旅行,全面阐述委托及其核心话题,逐一梳理委托、委托链、事件等。
关于委托
了解委托先从其定义开始,通常一个委托被声明为:
public delegate void CalculateDelegate(int x,int y);
关键字delegate用于声明一个委托类型CalculateDelegate,可以对其添加访问修饰符,默认其返回类型为void,接受两个int类型的参数x和y,但是委托并不等于方法,而是一种引用类型,类似c++中的函数指针,指向一个方法。
下面的示例将介绍如何通过委托来实现一个计算器模拟程序,在基础上来了解关于委托的定义、创建和应用:
public class DelegateDemo { // 声明委托类型 public delegate void CalculateDelegate(Int32 x,Int32 y); public void Add(Int32 x, Int32 y) { Console.WriteLine(x + y); } // 定义委托类型的变量 private CalculateDelegate myDelegate; public void BeginCalcualte() { // 委托绑定 myDelegate = new CalculateDelegate(Add); //调用委托 myDelegate(1,2); } }
上述示例中,在DelegateDemo类中声明了一个CalculateDelegate的委托类型,它具有和绑定方法Add完全相同的返回类型和参数,否则无法通过编译,将方法传给CalculateDelegate的构造器,也就是将方法指派给委托CalculateDelegate委托,并将该引用赋给myDelegate变量,也就表示myDelegate变量保存指向了Add方法的引用,以此实现对Add的回调。
由此可见,委托表示了对其回调方法的签名,可以将方法当作参数来传递,并根据传入的方法来动态的调用方法,所以,只要提供和委托具有相同签名的方法就可以与委托绑定,例如:
public void Sub(Int32 x, Int32 y) { Console.WriteLine(x - y); }
同样可以将Sub分配给委托,如下:
// 委托绑定 myDelegate = new CalculateDelegate(Sub); //调用委托 myDelegate(2, 1);
多播委托委托链
在上述委托的实现中,Add方法和Sub方法可以绑定到同一个类型的委托上,那么它们可不可以绑定到同一个委托变量上呢?答案是可以,多个方法可以绑定到同一个委托变量上,在委托变量做回调的时候可以依次执行其绑定的方法,这种技术称为多播委托。在.NET中提供了相当简洁的语法类创建委托链,以+=和-=操作符来分别进行绑定和解除绑定操作,多个方法绑定到同一个委托变量就形成了一条委托链,对其调用时会以此调用所有绑定的回调方法。例如:
public void BeginCalcualte() { // 委托绑定 myDelegate = new CalculateDelegate(Add); myDelegate += new CalculateDelegate(Sub); //调用委托 myDelegate(211, 99); }
计算结果为 :210 112
再以-=来解除绑定
myDelegate -= new CalculateDelegate(Sub);
//调用委托
myDelegate(211, 99);
计算结果为:310,可见通过-=操作,解除了Sub方法。
事实上+=和-=操作分别调用了Delegate.Combine和Delegate.Remove方法,委托本质上仍然是一个类,如此简洁的语法正式因为CLR和编译在后台完成了一系列操作。
.NET的事件模型建立在委托的机制上,可以说事件是对委托的封装,从委托的示例中可知,在客户端可以随意对委托进行操作,一定程度上破化了面向对象的封装机制,因此事件实现了对委托的封装。
下面通过将委托的示例进行改造,来完成一个事件的定义过程:
public class Calculator { // 用来存放事件引发时向处理程序传递的状态信息 public class CalculateEventArgs : EventArgs { public readonly Int32 x, y; public CalculateEventArgs(Int32 x, Int32 y) { this.x = x; this.y = y; } } //声明事件委托 public delegate void CalculateEventHander(object sender,CalculateEventArgs e); //定义事件成员 提供外部绑定 public event CalculateEventHander myCalculate; protected virtual void OnCalculate(CalculateEventArgs e) { if(myCalculate!=null) { myCalculate(this,e); } } //进行计算,调用该方法表示有新的计算发生 public void Calculate(Int32 x, Int32 y) { CalculateEventArgs e = new CalculateEventArgs(x,y); //通知所有事件 的注册者 OnCalculate(e); } }
示例中,对计算器模拟程序做了简要修改,从二者的对比中可以体会出事件的完整定义过程,主要包括:
- 定义一个内部事件参数类型,用于存放事件引发时向事件处理程序传递的状态信息,EventArgs是事件数据类的基类。
- 声明事件委托,主要包括两个参数:一个表示事件的发送者对象,一个表示时间参数类对象。
- 定义事件成员。
- 定义负责通知事件引发的方法,它被实现为Protected virtual方法,目的是可以在派生类中覆写该方法来拒绝监听事件。
- 定义一个触发事件的犯法,例如Calculate被调用时,表示有新的计算发生。
一个事件的完整程序就这样定义好了,然后还需要定义一个事件触发程序用来监听事件:
public class CalculateManager { public void Add(object sender, Calculator.CalculateEventArgs e) { Console.WriteLine(e.x+e.y); } public void Sub(object sender, Calculator.CalculateEventArgs e) { Console.WriteLine(e.x - e.y); } }
最后,在客户端 实现事件处理程序:
static void Main(string[] args) { Calculator calculator = new Calculator(); //事件监听者 CalculateManager cm = new CalculateManager(); //事件绑定 calculator.myCalculate += cm.Add; calculator.Calculate(200,100); calculator.myCalculate += cm.Sub; calculator.Calculate(200, 100); calculator.myCalculate -= cm.Sub; Console.ReadLine(); }
如果对设计模式有所了解,上述实现过程实质是观察者模式在委托中的应用。