C#基础篇——事件

前言

在本章中,主要是借机这个C#基础篇的系列整理过去的学习笔记、归纳总结并更加理解透彻。

在上一篇文章,我们已经对委托有了进一步了解,委托相当于用方法作为另一方法参数,同时,也可以实现在两个不能直接调用的方法中做桥梁。

下面我们来回顾一下委托的例子。.

   public delegate void ExecutingDelegate(string name);

   public class ExecutingManager
  {
       public void ExecuteProgram(string name, ExecutingDelegate ToExecuting)
      {
           ToExecuting(name);
      }
  }
   private static void StartExecute(string name)
  {
       Console.WriteLine("开始执行:" + name);
  }

   private static void EndExecute(string name)
  {
       Console.WriteLine("结束执行:" + name);
  }

   static void Main(string[] args)
  {
       ExecutingManager exec = new ExecutingManager();
       exec.ExecuteProgram("开始。。。", StartExecute);
       exec.ExecuteProgram("结束。。。", EndExecute);
       Console.ReadKey();
  }

根据上述的示例,再利用上节学到的知识,将多个方法绑定到同一个委托变量实现多播,该如何做呢?

再次修改代码:

   static void Main(string[] args)
  {
       ExecutingManager exec = new ExecutingManager();
       ExecutingDelegate executingDelegate;
       executingDelegate = StartExecute;
       executingDelegate += EndExecute;
       exec.ExecuteProgram("yuan", executingDelegate);

       Console.ReadKey();
  }

但是,此刻我们发现是不是可以将实例化声明委托的变量封装到ExecutingManager类中,这样是不是更加方便调用呢?

   public class ExecutingManager
  {
       /// <summary>
       /// 在 ExecutingManager 类的内部声明 executingDelegate 变量
       /// </summary>
       public ExecutingDelegate executingDelegate;
       public void ExecuteProgram(string name, ExecutingDelegate ToExecuting)
      {
           ToExecuting(name);
      }
  }
   static void Main(string[] args)
  {
       ExecutingManager exec = new ExecutingManager();
       exec.executingDelegate = StartExecute;
       exec.executingDelegate += EndExecute;
       exec.ExecuteProgram("yuan", exec.executingDelegate);
       Console.ReadKey();
  }

写到这里了,这样做没有任何问题,但我们发现这条语句很奇怪。在调用exec.ExecuteProgram方法的时候,再次传递了exec的executingDelegate字段, 既然如此,我们何不修改 ExecutingManager类成这样:

   public class ExecutingManager
  {
       /// <summary>
       /// 在 GreetingManager 类的内部声明 delegate1 变量
       /// </summary>
       public ExecutingDelegate executingDelegate;
       public void ExecuteProgram(string name)
      {
           if (executingDelegate != null) // 如果有方法注册委托变量
          {
               executingDelegate(name); // 通过委托调用方法
          }
      }
  }
   static void Main(string[] args)
  {
       ExecutingManager exec = new ExecutingManager();
       exec.executingDelegate = StartExecute;
       exec.executingDelegate += EndExecute;
       exec.ExecuteProgram("yuan");
       Console.ReadKey();
  }

这样再看,发现调用一下就更加简洁了。

正文

在日常生活中,我们可能都会遇到这样的各种各样的事情,而对于这些事情我们都会采取相应的措施。比如,当你要给一个女神过生日的时候,你就可以给她送礼物。而这种情况,在C#开发中,就相当于过生日被当作事件来对待,而送礼物就是事件做出的响应。

当女神过生日的时候,女神就会发布生日事件,而你就会接受到这个事件的通知,并做出响应的处理(送礼物等骚操作)。其中,触发这个事件的对象我们可称之为事件发布者,而捕获这个事件并做出相应处理的称之为事件订阅者,我们可以看出,女神就是充当了发布者,而你自己则充当了订阅者。

这里由生日事件引申出两类角色,即事件发布者事件订阅者

开始

1.发布者/订阅者模式

在开发中,我们是否遇到这样的情景,当一个特定的程序事件发生时,其他程序部分可以得到该事件注册发生通知。

发布者定义一系列事件,并提供一个注册方法;订阅者向发布者注册,并提供一个可被回调的方法,也就是事件处理程序;当事件被触发的时候,订阅者得到通知,而订阅者所提交的所有方法会被执行。

  • 发布者:发布某个事件的类或结构,其他类可以在该事件发生时得到通知。

  • 订阅者:注册并在事件发生时得到通知的类或结构。

  • 事件处理程序:由订阅者注册到事件的方法,在发布者触发事件时执行。事件处理程序方法可以定义在事件所在的类或结果中,也可以定义在不同的类或结构中。

  • 触发事件:调用事件的术语。当事件触发时,所有注册到它的方法都会被一次调用。

2.基本使用

       /// <summary>
       /// 先自定义一个委托
       /// </summary>
       /// <param name="oldPrice"></param>
       /// <param name="newPrice"></param>
       public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);
       /// <summary>
       /// 这个一个发布者
       /// </summary>
       public class IPhone
      {
           decimal price;
           /// <summary>
           /// 定义一个事件
           /// event 用来定义事件
           /// PriceChangedHandler委托类型,事件需要通过委托来调用订阅者需要的方法
           /// </summary>
           public event PriceChangedHandler PriceChanged;
         
           public decimal Price
          {
               get { return price; }
               set
              {
                   if (price == value)
                       return;
                   decimal oldPrice = price;
                   price = value;             // 如果调用列表不为空,则触发。           
                   if (PriceChanged != null)   //用来判断事件是否被订阅者注册过
                       PriceChanged(oldPrice, price);   //调用事件
              }
          }
      }
       /// <summary>
       /// 这个一个订阅者
       /// </summary>
       /// <param name="oldPrice"></param>
       /// <param name="price"></param>
       static void iPhone_PriceChanged(decimal oldPrice, decimal price)
      {
           Console.WriteLine("618促销活动,全场手机 只卖 " + price + " 元, 原价 " + oldPrice + " 元,快来抢!");
      }
       static void Main()
      {
           ///实例化一个发布者类
           IPhone phone = new IPhone()
          {
               Price = 5288
          };         // 订阅事件  
           phone.PriceChanged += iPhone_PriceChanged;          //完成事件的注册 调整价格(事件发生)    
           phone.Price = 3999;                                //激发事件,并调用事件
           Console.ReadKey();
      }

输出:

618促销活动,全场手机 只卖 3999 元, 原价 5288 元,快来抢!

3.解析

  1. 委托类型声明:事件与事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。

  2. 事件声明:使用关键字evet来声明一个事件,当声明的事件为一个public时,称为发布了一个事件。

  3. 事件注册:订阅者通过+=操作符来注册事件,并提供一个事件处理程序。

  4. 事件处理程序:订阅者向事件注册的方法,它可以是显示命名的方法、匿名方法或者Lambda表达式

  5. 触发事件:发布者用来调用事件的代码

4.语法

事件的声明语法:

//声明一个事件
public [static] event EventHandler EventName;
//声明多个同类型的事件
public [static] event EventHandler EventName1, EventName2, EventName3;

事件必须声明在类或结构中,因为事件它不是一个类型,它是一个类或者结构中的一员。

在事件被触发之前,可以通过和null做比较,判断是否包含事件注册处理程序。因为事件成员被初始化默认是null。

委托类型EventHandler是声明专门用来事件的委托。事件提供了对委托的结构化访问;也即是无法直接访问事件中的委托。

5.用法

C#基础篇——事件

查看源码:

事件的标准模式就是System命名空间下声明的EventHandler委托类型。

EventArgs是System下的一个类,如下:

using System.Runtime.InteropServices;

namespace System
{
  [Serializable]
  [ComVisible(true)]
  [__DynamicallyInvokable]
   public class EventArgs
  {
      [__DynamicallyInvokable]
       public static readonly EventArgs Empty = new EventArgs();

      [__DynamicallyInvokable]
       public EventArgs()
      {
      }
  }
}

根据EventArgs源码看出,EventArgs本身无法保存和传递数据的。

如果想保存和传递数据,可以实现一个EventArgs的派生类,然后定义相关的字段来保存和传递参数。

   public class IPhone
  {
       decimal price;
       /// <summary>
       /// 使用EventHandler定义一个事件
       /// </summary>
       public event EventHandler PriceChanged;
       protected virtual void OnPriceChanged()
      {
           if (PriceChanged != null)
               PriceChanged(this, null);
      }
       public decimal Price
      {
           get { return price; }
           set
          {
               if (price == value) return;
               decimal oldPrice = price;
               price = value;             // 如果调用列表不为空,则触发。     
               if (PriceChanged != null)  // //用来判断事件是否被订阅者注册过
                   OnPriceChanged();
          }
      }
  }
   /// <summary>
   /// 这个一个订阅者
   /// </summary>
   /// <param name="sender"></param>
   /// <param name="e"></param>
   static void iphone_PriceChanged(object sender, EventArgs e)
  {
       Console.WriteLine("年终大促销,快来抢!");
  }
   static void Main()
  {
       IPhone phone = new IPhone()
      {
           Price = 5288M
      };         // 订阅事件  
       phone.PriceChanged += iphone_PriceChanged;
       // 调整价格(事件发生)  
       phone.Price = 3999;
       Console.ReadKey();
  }

通过扩展EventHanlder来传递数据

C#基础篇——事件

System下另有泛型EventHandler类。由此,这里我们可以将派生于EventArgs的类作为类型参数传递过来,这样,既可以获得派生类保存的数据。

   ///扩展类
   public class PriceChangedEventArgs : System.EventArgs
  {
       public readonly decimal OldPrice;
       public readonly decimal NewPrice;
       public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
      {
           OldPrice = oldPrice;
           NewPrice = newPrice;
      }
  }
   public class IPhone
  {
       decimal price;
       public event EventHandler<PriceChangedEventArgs> PriceChanged;
       protected virtual void OnPriceChanged(PriceChangedEventArgs e)
      {
           if (PriceChanged != null)
               PriceChanged(this, e);
      }
       public decimal Price
      {
           get { return price; }
           set
          {
               if (price == value) return;
               decimal oldPrice = price;
               price = value;             // 如果调用列表不为空,则触发。     
               if (PriceChanged != null)
                   OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
          }
      }
  }
   static void iphone_PriceChanged(object sender, PriceChangedEventArgs e)
  {
       Console.WriteLine("618促销活动,全场手机 只卖 " + e.NewPrice + " 元, 原价 " + e.OldPrice + " 元,快来抢!");
  }
   static void Main()
  {
       IPhone phone = new IPhone()
      {
           Price = 5288M
      };         // 订阅事件  
       phone.PriceChanged += iphone_PriceChanged;
       // 调整价格(事件发生)  
       phone.Price = 3999;
       Console.ReadKey();
  }

输出

618促销活动,全场手机 只卖 3999 元, 原价 5288 元,快来抢!

6.移除事件

可以利用 -= 运算符处理程序从事件中移除,当程序处理完后,可以将事件从中把它移除掉。

       class Publiser
      {
           public event EventHandler SimpleEvent;

           public void RaiseTheEvent()
          {
               SimpleEvent(this, null);
          }
      }

       class Subscriber
      {
           public void MethodA(object o, EventArgs e) { Console.WriteLine("A"); }
           public void MethodB(object o, EventArgs e) { Console.WriteLine("B"); }
      }


       static void Main(string[] args)
      {
           Publiser p = new Publiser();
           Subscriber s = new Subscriber();

           p.SimpleEvent += s.MethodA;
           p.SimpleEvent += s.MethodB;
           p.RaiseTheEvent();

           Console.WriteLine("\n移除B事件处理程序");
           p.SimpleEvent -= s.MethodB;
           p.RaiseTheEvent();

           Console.ReadKey();
      }

输出:

C#基础篇——事件

7.事件访问器

运算符+= 、-=事件允许的唯一运算符。这些运算符是有预定义的行为。然而,我们可以修改这些运算符的行为,让事件执行任何我们希望定义的代码。

可以通过为事件定义事件访问器,来控制事件运算符+=、-=运算符的行为

  1. 两个访问器:add 和 remove

  2. 声明事件的访问器看上去和声明一个熟悉差不多。

下面示例演示了具有访问器的声明.两个访问器都有叫做value的隐式值参数,它接受实例或静态方法的引用

public event EventHandler Elapsed
{
   add
  {
       //... 执行+=运算符的代码
  }

    remove
    {
       //... 执行-=运算符的代码
    }

}

声明了事件访问器后,事件不包含任何内嵌委托对象.我们必须实现自己的机制来存储和移除事件的方法。

事件访问器表现为void方法,也就是不能使用会返回值的return语句。

示例:

       //声明一个delegate
       delegate void EventHandler();

       class MyClass
      {
           //声明一个成员变量来保存事件句柄(事件被激发时被调用的delegate)
           private EventHandler m_Handler = null;

           //激发事件
           public void FireAEvent()
          {
               if (m_Handler != null)
              {
                   m_Handler();
              }
          }

           //声明事件
           public event EventHandler AEvent
          {
               //添加访问器
               add
              {
                   //注意,访问器中实际包含了一个名为value的隐含参数
                   //该参数的值即为客户程序调用+=时传递过来的delegate
                   Console.WriteLine("AEvent add被调用,value的HashCode为:" + value.GetHashCode());
                   if (value != null)
                  {
                       //设置m_Handler域保存新的handler
                       m_Handler = value;
                  }
              }

               //删除访问器
               remove
              {
                   Console.WriteLine("AEvent remove被调用,value的HashCode为:" + value.GetHashCode());
                   if (value == m_Handler)
                  {
                       //设置m_Handler为null,该事件将不再被激发
                       m_Handler = null;
                  }
              }

          }

      }

       static void Main(string[] args)
      {
           MyClass obj = new MyClass();
           //创建委托
           EventHandler MyHandler = new EventHandler(MyEventHandler);
           MyHandler += MyEventHandle2;
           //将委托注册到事件
           obj.AEvent += MyHandler;
           //激发事件
           obj.FireAEvent();
           //将委托从事件中撤销
           obj.AEvent -= MyHandler;
           //再次激发事件
           obj.FireAEvent();


           Console.ReadKey();
      }
       //事件处理程序
       static void MyEventHandler()
      {
           Console.WriteLine("This is a Event!");
      }

       //事件处理程序
       static void MyEventHandle2()
      {
           Console.WriteLine("This is a Event2!");
      }

输出:

C#基础篇——事件

总结

  1. 这节对事件的基本使用,以及事件的标准语法、事件访问器等多个地方进行说明,大致可以了解和掌握事件的基本使用。

  2. 结合上一篇的委托和这一节的事件,委托和事件我们大概掌握了基本用法。并加以实践,结合实际开发,应用其中。

  3. 如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。