C#中的委托和事件的概念接触很久了,但是一直以来总没有特别透彻的感觉,现在我在这里总结一下:
首先我们要知道委托的由来,为什么要使用委托了?
我们先看一个例子:
假设我们有这样一个需求,需要计算在不同方式下的总价,如下面代码所示,这里假设只有两种方式,一种是正常价格,一种是折扣价格:
1 public enum CalcMethod 2 { 3 Normal, 4 Debate 5 } 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 CalcPrice(10, 100,CalcMethod.Normal); 11 CalcPrice(10, 100, CalcMethod.Debate); 12 Console.ReadLine(); 13 } 14 15 /// <summary> 16 /// 计算总价 17 /// </summary> 18 /// <param name="count">数量</param> 19 /// <param name="price">单价</param> 20 public static double CalcPrice(int count,int price,CalcMethod method) 21 { 22 switch (method) 23 { 24 case CalcMethod.Normal: 25 return NormalPrice(count, price); 26 case CalcMethod.Debate: 27 return DebatePrice(count, price); 28 default: 29 return 0; 30 } 31 } 32 33 public static double NormalPrice(int count,int price) 34 { 35 Console.WriteLine("正常的价格是:{0}", count * price); 36 return count * price; 37 } 38 public static double DebatePrice(int count, int price) 39 { 40 Console.WriteLine("折扣的价格是:{0}", count * price*0.7); 41 return count * price*0.7; 42 } 43 }
但是我们想一想,如果还要增加总价计算方式,那么我们是不是要不断的修改CalcPrice方法,CalcMethod枚举呢?
那么是不是有更好的方式呢?
我们可以把真正的计算价格的方式委托给一个函数来计算,这样委托就诞生了。
首先我们定义一个跟方法参数和返回值类型一样的委托类型:
public delegate double CalcPriceDelegate(int count, int price);
然后修改计算方法:
/// <summary>
/// 计算总价
/// </summary>
/// <param name="count">数量</param>
/// <param name="price">单价</param>
public static double CalcPrice(int count, int price, CalcPriceDelegate calcDelegate)
{
return calcDelegate(count, price);
}
然后调用的时候直接用签名相同的方法传递就可以了:
CalcPrice(10, 100, NormalPrice);
CalcPrice(10, 100, DebatePrice);
到这里我们大体明白了委托可以使用方法作为参数,这样就避免了程序中出现大量的条件分支语句,程序的扩展性好。
接下来我要对委托做一个深入的探讨:
委托首先其实也是一个类,
public delegate double CalcPriceDelegate(int count, int price);
上面这句话其实就是申明一种委托类型,这个类型在编译的时候会生成以下成员:
1)public extern CalcPriceDelegate(object @object, IntPtr method);
第一个参数是记录委托对象包装的实例方法所在的对象(this,如果包装的是静态方法,就为NULL),第二个参数就是表示要回调的方法。
2) public virtual extern double Invoke(int count, int price);//同步调用委托方法
3)public virtual extern IAsynResult BeginInvoke(int count, int price, AsyncCallback callback, object @object); 这个是异步执行委托方法,前面两个参数是委托的方法的输入参数,callback是回调方法,也就是说方法执行完成后的回调方法,AsyncCallback本身也是一个委托类型,其原型是:
public delegate void AsyncCallback(IAsyncResult ar);
最后一个参数是回调所需要的输入参数,这个参数会隐含在IAsyncResult的AsyncState中。
另外返回值也是一个IAsyncResult结果。
与之相对应的,public virtual extern double EndInvoke(IAsyncResult result)
结束异步回调。
以前对IAsyncResult,还有AsyncCallback都有点陌生,其实我们可以这样来理解,我想要一个方法异步来执行,那么肯定就需要调用BeginInvoke,那么我如何又能知道什么时候这个异步的调用结束呢?这就需要用到AsyncCallback这个异步回调,如果这个回调需要参数,就赋值给object,如果不确定是否异步执行完,就要用EndInvoke来确保结束,输入参数就是BeginInvoke的返回值IAsynResult,就相当于BeginInvoke的时候开出了一个收据,EndInvoke又把这个收据还了。
这个话题要想深入下去就太多了,我们还是回到委托上来,委托时一个类,其编译后就是这么些个成员。
我平时调用的时候经常会被各种各样的调用方法给搞糊涂了,这里总结下各种调用方式:
假设委托类型为:public delegate double CalcPriceDelegate(int count, int price); 这就相当于定义了一个类,
接下来就是赋值了(相当于申明对象):
1)CalcPriceDelegate calcDelegate = new CalcPriceDelegate(NormalPrice). 这是最完整的赋值方式。
2) CalcPriceDelegate calcDelegate = NormalPrice; 直接赋值方法,编译器会自动帮我们构造成第一种赋值方式。
赋值完成后接下来就是如何调用了:
1)calcDelegate(10,100);
2)calcDelegate.Invoke(10,100) 与1)方法是一样的。
3)calcDelegate.BeginInvoke(10,100,null,null) 异步执行
委托还可以通过+=来添加方法,委托给多个方法,但是第一个必须是=,否则没有初始化,相对的,可以使用-=来移除方法。
至于匿名委托,lambda表达式是一样的,把握本质就可以了,还需要了解MS定义的委托类型,这里暂时不讲了。
接下来讲事件:
第一步,我们还是引用上面的例子,只是把相关的代码放到一个类里面:
1 public delegate double CalcPriceDelegate(int count, int price); 2 public class CalcPriceClass 3 { 4 public double Calc(int count, int price, CalcPriceDelegate calcDelegate) 5 { 6 return calcDelegate(count, price); 7 } 8 }
其中主函数里面的调用如下:
1 Console.WriteLine("演示引入事件的第一步:"); 2 CalcPriceClass cp = new CalcPriceClass(); 3 cp.Calc(10, 100, NormalPrice); 4 cp.Calc(10, 100, DebatePrice); 5 Console.ReadLine();
这种方法,我们破坏了对象的封装性,我们可以把委托类型的变量放到CalcPriceClass类里面。
于是我们就有了第二步:
1 public class CalcPriceClass2 2 { 3 public CalcPriceDelegate m_delegate; 4 public double Calc(int count, int price, CalcPriceDelegate calcDelegate) 5 { 6 return calcDelegate(count, price); 7 } 8 }
其中主函数的调用如下:
1 Console.WriteLine("演示引入事件的第二步:"); 2 CalcPriceClass2 cp2 = new CalcPriceClass2(); 3 cp2.m_delegate = NormalPrice; 4 cp2.m_delegate += DebatePrice; 5 cp2.Calc(10, 100, cp2.m_delegate); 6 Console.ReadLine();
我们发现其调用有点怪怪的, cp2.Calc(10, 100, cp2.m_delegate);既然我为cp2的委托对象赋值了,这个时候其实没有必要再去传递这个委托对象了,于是就有了第三步:
1 public class CalcPriceClass3 2 { 3 public CalcPriceDelegate m_delegate; 4 public double Calc(int count, int price) 5 { 6 if (m_delegate != null) 7 return m_delegate(count, price); 8 else return 0; 9 } 10 }
其中主函数的调用如下:
1 Console.WriteLine("演示引入事件的第三步:"); 2 CalcPriceClass3 cp3 = new CalcPriceClass3(); 3 cp3.m_delegate = NormalPrice; 4 cp3.m_delegate += DebatePrice; 5 cp3.Calc(10, 100); 6 Console.ReadLine();
在这步完成后,貌似达到了我们想要的结果,但是还是有些隐患的,因为我们可以随意的给委托变量赋值,所以就有了第四步,加上了事件:
1 public class CalcPriceClass4 2 { 3 public event CalcPriceDelegate m_delegate; 4 public double Calc(int count, int price) 5 { 6 if (m_delegate != null) 7 return m_delegate(count, price); 8 else return 0; 9 } 10 }
其中主函数的调用如下:
1 Console.WriteLine("演示引入事件的第四步:"); 2 CalcPriceClass4 cp4 = new CalcPriceClass4(); 3 cp4.m_delegate += NormalPrice; 4 cp4.m_delegate += DebatePrice; 5 cp4.Calc(10, 100); 6 Console.ReadLine();
我们给委托变量加上event后有什么不一样呢?
这个时候我们不能直接给这个事件对象进行赋值,因为其内部是一个私有变量了,另外编译器会增加两个公共函数,
一个是add_m_delegate(对应+=),
public void add_m_delegate(CalcPriceDelegate value)
{
this.m_delegate = (CalcPriceDelegate)Delegate.Combine(this.m_delegate, value);
}
一个是remove_m_delegate(对应-=),
public void remove_m_delegate(CalcPriceDelegate value)
{
this.m_delegate = (CalcPriceDelegate)Delegate.Remove(this.m_delegate, value);
}
另外内部的私有字段是这样子的:private CalcPriceDelegate m_delegate;
通过这四步,我们可以知道了从委托到事件的一个过程,其实事件也是委托,只是编译器会帮我们做一些事情而已。
事件 与 Observer设计模式
假设一个热水器,在温度达到95度以上的时候,警报器报警,并且显示器显示温度。
代码如下:
1 public class Heater 2 { 3 private int temperature; 4 public void BoilWater() 5 { 6 for (int i = 0; i < 100; i++) 7 { 8 temperature = i; 9 if (temperature > 95) 10 { 11 MakeAlert(temperature); 12 ShowMsg(temperature); 13 } 14 } 15 } 16 private void MakeAlert(int param) 17 { 18 Console.WriteLine("Alarm:滴滴滴,水已经{0}度了", param); 19 20 } 21 private void ShowMsg(int param) 22 { 23 Console.WriteLine("Display:水快开了,当前温度:{0}度。", param); 24 } 25 }
假设热水器由三部分组成:热水器、警报器、显示器,它们来自于不同厂商并进行了组装。那么,应该是热水器仅仅负责烧水,它不能发出警报也不能显示水温;在水烧开时由警报器发出警报、显示器显示提示和水温。如果是上面的代码可能就不合适了,就要使用下面的代码:
1 public class Heater2 2 { 3 private int temperature; 4 public delegate void BoilHanlder(int param); 5 public event BoilHanlder BoilEvent; 6 public void BoilWater() 7 { 8 for (int i = 0; i < 100; i++) 9 { 10 temperature = i; 11 if (temperature > 95) 12 { 13 if (BoilEvent != null) 14 BoilEvent(temperature); 15 } 16 } 17 } 18 } 19 public class Alarm 20 { 21 public static void MakeAlert(int param) 22 { 23 Console.WriteLine("Alarm:滴滴滴,水已经{0}度了", param); 24 25 } 26 } 27 public class Display 28 { 29 public static void ShowMsg(int param) 30 { 31 Console.WriteLine("Display:水快开了,当前温度:{0}度。", param); 32 } 33 }
调用的代码:
1 Console.WriteLine("演示热水器热水机报警及显示水温 观察者模式"); 2 Heater2 ht2 = new Heater2(); 3 ht2.BoilEvent += new Heater2.BoilHanlder(Alarm.MakeAlert); 4 ht2.BoilEvent += Display.ShowMsg; 5 ht2.BoilWater(); 6 Console.ReadLine();
比较以上两种方式的不同,后面的这种更加符合面向对象的思想,因为作为热水器而言,主要的工作是热水,至于报警,显示温度是由其他器件来显示,是需要显示器,报警器这些观察者来观察热水器这个观察对象主体的温度。
到这里为止,我们知道了如何声明委托类型,申明委托对象,调用委托方法,委托类的实际成员,以及从委托到事件的演变,事件的表象与在编译后的实际成员,以及作为观察者模式使用事件的过程。
不过微软的规范写法却不是这样,下面改用微软的规范写法:
1 public class Heater3 2 { 3 public string type = "RealFire 001"; 4 public string area = "China Xian"; 5 private int temperature; 6 public delegate void BoiledEventHandler(object sender,BoiledEventArgs e); 7 public event BoiledEventHandler Boiled; 8 protected virtual void OnBoiled(BoiledEventArgs e) 9 { 10 if (Boiled != null) 11 { 12 Boiled(this, e); 13 } 14 } 15 public void BoilWater() 16 { 17 for (int i = 0; i < 100; i++) 18 { 19 temperature = i; 20 if (temperature > 95) 21 { 22 BoiledEventArgs e = new BoiledEventArgs(temperature); 23 OnBoiled(e); 24 } 25 } 26 } 27 } 28 public class BoiledEventArgs : EventArgs 29 { 30 public readonly int temperature; 31 public BoiledEventArgs(int temp) 32 { 33 temperature = temp; 34 } 35 } 36 public class Alarm3 37 { 38 public void MakeAlert(object sender, BoiledEventArgs e) 39 { 40 Heater3 ht = (Heater3)sender; 41 Console.WriteLine("Alarm:{0}-{1}", ht.area, ht.type); 42 Console.WriteLine("Alarm:滴滴滴,水已经{0}度了:", e.temperature); 43 Console.WriteLine(); 44 } 45 } 46 public class Display3 47 { 48 public static void ShowMsg(object sender, BoiledEventArgs e) 49 { 50 Heater3 ht = (Heater3)sender; 51 Console.WriteLine("Display:{0}-{1}",ht.area,ht.type); 52 Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", e.temperature); 53 Console.WriteLine(); 54 } 55 56 }
调用代码:
1 Console.WriteLine("演示热水器热水机报警及显示水温 符合微软模式"); 2 Heater3 ht3 = new Heater3(); 3 Alarm3 al3 = new Alarm3(); 4 ht3.Boiled += al3.MakeAlert; 5 ht3.Boiled += Display3.ShowMsg; 6 ht3.BoilWater(); 7 Console.ReadLine();
微软的规范写法,如果要传递参数一般使用EventArgs或者其继承类,且继承类的命名以EventArgs结尾。
委托类型的名称以EventHandler结束。
委托的原型定义:有一个void返回值,并接受两个输入参数:一个Object 类型,一个 EventArgs类型(或继承自EventArgs)。
一般这个Object类型指的是观察的对象,我们可以这样来记忆,因为委托就相当于方法,其实执行的就是观察者,那么观察者总要知道观察谁,以及观察所需要的参数。
其实事件还有一些概念:事件订阅者,事件接收者,事件发送者,这些以后再补充
这里引用了这位仁兄的博客:
http://www.tracefact.net/csharp-programming/delegates-and-events-in-csharp.aspx
代码:
http://files.cnblogs.com/files/monkeyZhong/CSharpDelegateAndEvent.zip