C#中的委托和事件的概念接触很久了,但是一直以来总没有特别透彻的感觉,现在我在这里总结一下:

首先我们要知道委托的由来,为什么要使用委托了?

我们先看一个例子:

假设我们有这样一个需求,需要计算在不同方式下的总价,如下面代码所示,这里假设只有两种方式,一种是正常价格,一种是折扣价格:

  1. public enum CalcMethod
  2. {
  3. Normal,
  4. Debate
  5. }
  6. class Program
  7. {
  8. static void Main(string[] args)
  9. {
  10. CalcPrice(, ,CalcMethod.Normal);
  11. CalcPrice(, , 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 ;
  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(, , NormalPrice);
  4. cp.Calc(, , 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(, , 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 ;
  9. }
  10. }

其中主函数的调用如下:

  1. Console.WriteLine("演示引入事件的第三步:");
  2. CalcPriceClass3 cp3 = new CalcPriceClass3();
  3. cp3.m_delegate = NormalPrice;
  4. cp3.m_delegate += DebatePrice;
  5. cp3.Calc(, );
  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 ;
  9. }
  10. }

其中主函数的调用如下:

  1. Console.WriteLine("演示引入事件的第四步:");
  2. CalcPriceClass4 cp4 = new CalcPriceClass4();
  3. cp4.m_delegate += NormalPrice;
  4. cp4.m_delegate += DebatePrice;
  5. cp4.Calc(, );
  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 = ; i < ; i++)
  7. {
  8. temperature = i;
  9. if (temperature > )
  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 = ; i < ; i++)
  9. {
  10. temperature = i;
  11. if (temperature > )
  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 = ; i < ; i++)
  18. {
  19. temperature = i;
  20. if (temperature > )
  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

    

C# 深入浅出 委托与事件的更多相关文章

  1. (.NET高级课程笔记)委托、事件总结

      1.委托的声明.实例化和调用 同样的,也可以把事务写成上面的形式 2.泛型委托---Func.Action 3.委托的意义:解耦 4.委托的意义:异步多线程 5.委托的意义:多播委托 6.观察者模 ...

  2. C#委托和事件的使用的意义

    转载自:https://www.cnblogs.com/yinqixin/p/5056307.html 每一个初学C#的程序猿,在刚刚碰到委托和事件的概念时,估计都是望而却步,茫然摸不到头脑的.百度一 ...

  3. .NET面试题系列[7] - 委托与事件

    委托和事件 委托在C#中具有无比重要的地位. C#中的委托可以说俯拾即是,从LINQ中的lambda表达式到(包括但不限于)winform,wpf中的各种事件都有着委托的身影.C#中如果没有了事件,那 ...

  4. .NET基础拾遗(4)委托、事件、反射与特性

    Index : (1)类型语法.内存管理和垃圾回收基础 (2)面向对象的实现和异常的处理基础 (3)字符串.集合与流 (4)委托.事件.反射与特性 (5)多线程开发基础 (6)ADO.NET与数据库开 ...

  5. [转载]C#深入分析委托与事件

    原文出处: 作者:风尘浪子 原文链接:http://www.cnblogs.com/leslies2/archive/2012/03/22/2389318.html 同类链接:http://www.c ...

  6. [转载]C#委托和事件(Delegate、Event、EventHandler、EventArgs)

    原文链接:http://blog.csdn.net/zwj7612356/article/details/8272520 14.1.委托 当要把方法作为实参传送给其他方法的形参时,形参需要使用委托.委 ...

  7. C#委托与事件

    一.在控制台下使用委托和事件 我们都知道,C#中有"接口"这个概念,所谓的"接口"就是定义一套标准,然后由实现类来具体实现其中的方法,所以说"接口,是 ...

  8. C#委托与事件的简单使用

    前言:上一篇博文从原理和定义的角度介绍了C#的委托和事件.本文通过一个简单的小故事,来说明C#委托与事件的使用方法及其方便之处. 在阅读本文之前,需要你对委托和事件的基本概念有所了解.如果你是初次接触 ...

  9. C#之委托与事件

    委托与事件 废话一堆:网上关于委托.事件的文章有很多,一千个哈姆雷特就有一千个莎士比亚,以下内容均是本人个人见解. 1. 委托 1.1 委托的使用 这一小章来学习一下怎么简单的使用委托,了解一些基本的 ...

随机推荐

  1. java中String s="abc"及String s=new String("abc")详解

    1.   栈(stack)与堆(heap)都是Java用来在Ram中存放数据的地方.与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆. 2.   栈的优势是,存取速度比堆要快,仅次于直 ...

  2. Apache HTTP Server mod_session_dbd模块mod_session_dbd.c 安全漏洞

    漏洞名称: Apache HTTP Server mod_session_dbd模块mod_session_dbd.c 安全漏洞 CNNVD编号: CNNVD-201307-488 发布时间: 201 ...

  3. 嵌入式 linux 查看内存

    在Windows系统中查看内存的使用情况很简单,想必大家都已经耳熟能详了,那么在linux系统如何查看内存使用情况呢?下面和大家分享在Linux下查看内存使用情况的free命令: [root@scs- ...

  4. 数据结构(并查集):COGS 260. [NOI2002] 银河英雄传说

    260. [NOI2002] 银河英雄传说 ★★☆   输入文件:galaxy.in   输出文件:galaxy.out   简单对比时间限制:5 s   内存限制:128 MB [问题描述] 公元五 ...

  5. poj3261 Milk Patterns(后缀数组)

    [题目链接] http://poj.org/problem?id=3261 [题意] 至少出现k次的可重叠最长子串. [思路] 二分长度+划分height,然后判断是否存在一组的数目不小于k即可. 需 ...

  6. Java中sleep,wait,yield,join的区别

    sleep() wait() yield() join()用法与区别   1.sleep()方法 在指定时间内让当前正在执行的线程暂停执行,但不会释放“锁标志”.不推荐使用. sleep()使当前线程 ...

  7. uva 116 Unidirectional TSP (DP)

    uva 116 Unidirectional TSP Background Problems that require minimum paths through some domain appear ...

  8. 把安卓源代码中的system app独立出来,像开发普通app那样开发

          个人建议首先依照android源码的ide/eclipse中的格式化xml和import导入到你编译的eclipse中,假设你编译的android源码是2.3以上的版本号的,建议用JDK6 ...

  9. 百度地图api之如何自定义标注图标

    在百度地图api中,默认的地图图标是一个红色的椭圆形.但是在项目中常常要求我们建立自己的图标,类似于我的这个 操作很简单,分如下几步进行 步骤一:先ps一个图标,大小要合适,如果要背景透明的,记得保存 ...

  10. Exception in thread "main" brut.androlib.err.UndefinedResObject: resource spec: 0x01030200(转)

    反编译时遇到标题中的异常,根据描述,原因是找不到资源文件,最有可能的原因是apk中使用了系统资源. 解决办法如下: 从手机中导出framework-res.apk文件,该文件在/system/fram ...