C#中的委托相当于C++的函数指针或成员函数指针,不过与 C++ 函数指针不同,委托是完全面对对象的;另外,C++ 指针仅指向成员函数,而委托同时封装了对象实例和方法。
委托声明
委托是一种类型,一种引用类型,用来封装带有特定签名和返回类型的方法
委托利用关键定delegte创建,后跟一个返回类型和可以委托给它的方法的签名
public delegate int MyDge(object obj1, object obj2);
与类的定义一样,可以在委托声明前加上public这类关键字
注意,参数的名称不能省略
委托实例化
前面说过了,委托是一种类型,在使用时需要实例化
//仅声明 MyDge myDge1; //直接使用方法名 MyDge myDge2 = MothedA; MyDge myDge3 = this.MothedB; //使用new构造 myDge myDge4 = new MyDge(MothedA); MyDge myDge5 = new MyDge(this.MothedB); //使用匿名方法 int i; MyDge myDge6 = delegate(object a, object b) { i = 1; //do something }; //匿名方法以关键字delegate开头,后跟传给方法的参数,然后是大括号括起来的方法主体,方法中可以访问定义它们的作用域之内的变量,大括号以一个分号结尾
委托实例或者说委托变量myDge,实例上就是一个普通变量,可以作为类成员变量,也可以是局部变量,如果作为类成员,像所有成员变量一样,可以加上public、staitc这类关键字,还可以把它封装为属性。
public int Methed(object x, object y) { this.a = x; this.b = y; // do something }
mothed可以是类的静态方法,也可以是类的实例方法,前面说过“委托同时封装了对象实例和方法”,在这里,当mothed是类实例方法时,就体现了这一点。
委托调用
委托的调用语法与普通方法的调用语法是一致的(也提供了Invoke和BeginInvoke方法),且默认情况下,调用是同步的。
Console.Write("delegate call, the result:{0}", myDge2(a, b));
前面说过“委托同时封装了对象实例和方法”,如果myDge2封装的是类实例方法,那么
myDge(a, b)
知道在哪个类实例上调用这个方法
如果在对这样的委托进行调用期间发生异常,而且没有在被调用的方法内捕捉到该异常,则会在调用该委托的方法内继续搜索与该异常对应的 catch 子句,就像调用该委托的方法直接调用了该委托所引用的方法一样。
试图调用其值为 null 的委托实例将导致 System.NullReferenceException 类型的异常。
委托实例维护了一个调用列表,也就是说可以给一个委托实例添加多个方法(多重委托),委托实例所封装的方法集合称为调用列表。从某个方法创建一个委托实例时,该委托实例将封装此方法,此时,它的调用列表只包含一个进入点。但是,当组合两个非空委托实例时,它们的调用列表被连接在一起(按照左操作数在先、右操作数在后的顺序)以组成一个新的调用列表,它包含了两个或更多个“进入点”。
委托是使用二元 + 和 += 运算符来组合的。可以使用一元 - 和 -= 运算符将一个委托从委托组合中移除。委托间还可以进行比较以确定它们是否相等
MyDge d1 = new MyDge(MethedA); MyDge d2 = new MyDge(MethedB); MyDge d3 = d1 + d2 + d1; d3(a, b); //调用顺序:MethedA、MethedB、MethedA d3 -= d1; //也可以是 d3 -= MethedA d3(a, b); //调用顺序:MethedA、MethedB
如语句
d3 = d1 + d2 + d1;
中所显示,委托d1可以多次出现在一个调用列表中。这种情况下,它每出现一次,就会被调用一次。
在这样的调用列表中,当移除委托,如
d3 -= d1
实际上移除的是调用列表中最后出现的那个委托实例d1。试图从空的列表中移除委托(或者从非空列表中移除表中没有的委托)不算是错误
如果一个委托实例的调用列表包含多个进入点,那么调用这样的委托实例就是按顺序同步地调用调用列表中所列的各个方法。以这种方式调用的每个方法都使用相同的参数集,即提供给委托实例的参数集。如果这样的委托调用包含引用参数,那么每个方法调用都将使用对同一变量的引用;这样,若调用列表中有某个方法对该变量进行了更改,则调用列表中排在该方法之后的所有方法都会见到此变更。如果委托调用包含输出参数或一个返回值,则它们的最终值就是调用列表中最后一个方法调用所产生的结果。
获取多重委托中每个方法的返回值也是可能的,为此,必须接管多重委托所封装方法调用的职责,这需要获取委托实例的调用列表,并显式地依此调用每个封装的方法:
foreach(MyDge d in d3) { int reslut = d(a, b); Console.WriteLine("result:{0}", result); }
如果在处理此类委托的调用期间发生异常,而且没有在正被调用的方法内捕捉到该异常,则会在调用该委托的方法内继续搜索与该异常对应的 catch 子句,此时,调用列表中排在后面的任何方法将不会被调用。
事件
当为一个Form上的Buttom添加一个Click事件时,Form.Designer.cs中会被添加这样一行代码:
this.button1.Click += new System.EventHandler(this.button1_Click);
Click是Button类实例的成员变量,它是一个多重委托实例
而button1_Click定义如下:
private void button1_Click(object sender, EventArgs e) { //do something }
它是Form类实例的一个成员方法,或者说是一个事件处理方法
在C#中有这样一种模型,即发布和订阅模型,任何对象都可以发布(publish)一组事件供其他类订阅。当发布类产生事件时,所有订阅类都会得到通知
/// <summary> /// 事件类,继承自EventArgs /// </summary> public class TimeInfoEventArgs:EventArgs { public readonly int hour; public readonly int minute; public readonly int second; public TimeInfoEventArgs(int hour, int minute, int second) { this.hour = hour; this.minute = minute; this.second = second; } } /// <summary> /// 发布类 /// </summary> public class Clock { //声明委托类型 public delegate void SecondChangeHandler(object sender, TimeInfoEventArgs e); //定义委托实例 public SecondChangeHandler onSecondChange = null; private int second; //模拟消息泵 public void Run() { for(;;) { Thread.Sleep(10); System.DateTime dt = System.DateTime.Now; //触发事件(每秒触发一次) if(dt.Second != second) { TimeInfoEventArgs e = new TimeInfoEventArgs(dt.Hour, dt.Minute, dt.Second); //通知订阅者 if(onSecondChange != null) onSecondChange(this, e); } //更新状态 this.second = dt.Second; } } } /// <summary> /// 订阅类 /// </summary> public class DisplayClock { //事件方法 public void TimeHasChanged(object sender, TimeInfoEventArgs e) { Console.WriteLine("Current Time:{0}:{1}:{2}", e.hour.ToString(), e.minute.ToString(), e.second.ToString()); } //订阅事件 public void Subscribe(Clock c) { c.onSecondChange += new Clock.SecondChangeHandler(TimeHasChanged); } }
调用代码
//调用代码 Clock c = new Clock(); DisplayClock dc = new DisplayClock(); dc.Subscribe(c); c.Run();
Colck类在本地时间每改变一秒时,使用委托来知道可能的订阅者(可以有多个不的类作为订阅者),也就是说这种模型是基于多重委托实现的,发布类定义了订阅类必须实现的委托,并在适当的时候调用委托实例中的方法列表,订阅类实现匹配委托签名的方法,并使用+=将方法添加到发布类的委托实例中,发布者和订阅者通过委托实现了松耦合,其好处在于:Clock可以改变检测时间的方法,而不会对订阅者产生影响。订阅类(可以有多个)可以改变响应时间变化的方式,这也不会影响Clock,两者互相独立运作。使代码维护更加容易。
.net中的事件也是通过这种方式实现的,例如,Button在被按下时,可以告诉许多对此感兴的观察者类,这里按钮就称为发布者,因为它发布了Click事件,而其它类称为订阅者,因为它们订阅了Click事件,上述代码中的Clock相当于Button,而Display相当于Form。
.net中事件处理方法返回void,有两个参数,一个是事件的来源,也就是发布类,另一个是EventArgs派生而来的对象。推荐你的事件处理方法也遵循这种设计规范
与Clock不同的是,对于委托,事件使用了event关键字
public event SecondChangeHandler onSecondChange = null;
如果不使用这个关键字,DisplayClock中Subscribe方法可以有这样的代码
c.onSecondChange = new Clock.SecondChangeHandler(TimeHasChange);
=替代了+=,这可不好
在调用代码中,可以有这样的代码
//调用代码 Clock c = new Clock(); TimeInfoEventArgs e = new TimeInfoEventArgs(0, 0, 0); c.onSecondChange(c, e);
Clock类设计者的初衷是委托所封装的方法只在发生事件时才调用,而上述代码却饶了后门,自已调用了这些方法,还传入了假数据
event关键字能够告诉编译器,委托只能由其定义类调用,且类它类只能分别使用+=和-=操作符订阅和退订委托