zoukankan      html  css  js  c++  java
  • C# 深入浅出 委托与事件

    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     }
    View Code

    但是我们想一想,如果还要增加总价计算方式,那么我们是不是要不断的修改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     }
    View Code

            其中主函数里面的调用如下:

    1             Console.WriteLine("演示引入事件的第一步:");
    2             CalcPriceClass cp = new CalcPriceClass();
    3             cp.Calc(10, 100, NormalPrice);
    4             cp.Calc(10, 100, DebatePrice);
    5             Console.ReadLine();
    View Code

              这种方法,我们破坏了对象的封装性,我们可以把委托类型的变量放到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     }
    View Code

     其中主函数的调用如下:

    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();
    View Code

    我们发现其调用有点怪怪的,  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     }
    View Code

    其中主函数的调用如下:

    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();
    View Code

    在这步完成后,貌似达到了我们想要的结果,但是还是有些隐患的,因为我们可以随意的给委托变量赋值,所以就有了第四步,加上了事件:

     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     }
    View Code

    其中主函数的调用如下:

    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();
    View Code

    我们给委托变量加上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     }
    View Code

    假设热水器由三部分组成:热水器、警报器、显示器,它们来自于不同厂商并进行了组装。那么,应该是热水器仅仅负责烧水,它不能发出警报也不能显示水温;在水烧开时由警报器发出警报、显示器显示提示和水温。如果是上面的代码可能就不合适了,就要使用下面的代码:

     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     }
    View Code

    调用的代码:

    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();
    View Code

    比较以上两种方式的不同,后面的这种更加符合面向对象的思想,因为作为热水器而言,主要的工作是热水,至于报警,显示温度是由其他器件来显示,是需要显示器,报警器这些观察者来观察热水器这个观察对象主体的温度。
    到这里为止,我们知道了如何声明委托类型,申明委托对象,调用委托方法,委托类的实际成员,以及从委托到事件的演变,事件的表象与在编译后的实际成员,以及作为观察者模式使用事件的过程。

    不过微软的规范写法却不是这样,下面改用微软的规范写法:

     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     }
    View Code

    调用代码:

    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();
    View Code

    微软的规范写法,如果要传递参数一般使用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

        

  • 相关阅读:
    在Windows 10 64位上编译DCMTK
    python递归解决汉诺塔
    python迭代和递归实现斐波那契
    20199302 2019-2020-2 《网络攻防实践》第4周作业
    ssh爆破
    20199302 2019-2020-2 《网络攻防实践》第3周作业
    字符串模版替换的方法MessageFormat.format(String pattern, Object ... arguments)
    Java并发编程之LinkedBlockingDeque阻塞队列详解
    理解Spring容器、BeanFactory和ApplicationContext
    Steam之两个list间交集、并集、差集
  • 原文地址:https://www.cnblogs.com/monkeyZhong/p/4595444.html
Copyright © 2011-2022 走看看