首页 > 技术文章 > C# event EventHandler

cbltech 2021-03-30 10:44 原文

 

代码:public event EventHandler<NewMailEventArgs> NewMail;

C#编译器编译这行代码时,会把它翻译成以下3个构造:

1. 一个被初始化为null的私有委托字段
private EventHandler<NewMailEventArgs> NewMail = null;

2. 一个允许对象订阅事件的公共方法add_Xxx(其中Xxx是事件的名称)
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_NewMail(EventHandler<NewMailEventArgs> value)

{

    NewMail = (EventHandler<NewMailEventArgs>)
    Delegate.Combine(NewMail, value);
}

3. 一个允许对象注销事件的公共方法remove_Xxx(其中Xxx是事件的名称)
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_NewMail(EventHandler<NewMailEventArgs> value)

{

    NewMail = (EventHandler<NewMailEventArgs>)
    Delegate.Remove(NewMail, value);

}

第一个构造只是一个适当的委托类型的字段。这个字段引用的是一个委托链表的首部,当事件发生时,链表中的委托对象将被通知。该字段被初始化为null,这意味着刚开始没有监听者订阅这个事件。当有方法订阅事件时,该字段会指向一个EventHandler <NewMailEventArgs>委托的实例,该实例还可以引用额外的EventHandler <NewMailEventArgs>委托。当监听者订阅了事件时,它只需将一个委托类型的实例添加到该委托类型的链表上即可。显然,注销事件意味着从委托链表上移除相应的委托实例。

注意,委托字段,即本例中的NewMail,尽管在源代码中将事件定义为public,但委托字段也总是为private。将委托字段定义为私有方式可以防止类定义外的代码错误地操作该字段。如果该字段为公共字段,那么任何代码都可以改变字段的值,从而删除所有已订阅事件的委托实例。

C#编译器产生的第二个构造是一个允许其他对象订阅事件的方法。C#编译器通过在事件名称 (NewMail)前添加add_自动地命名方法。C#编译器还自动地为方法产生代码。产生的代码通常调用System.Delegate的静态 Combine方法,并将委托实例添加到委托链表上,然后返回新的链表首部(得到已保存到字段中的内容)。 
当C#编译器看到使用-=操作符注销事件委托的代码时,会生成一个对事件的remove方法的调用。

mm.remove_NewMail(new EventHandler<NewMailEventArgs>(FaxMsg));

和+ =操作符一样,即使使用的编程语言不直接支持事件,我们仍然可以通过显式地调用remove访问器方法来注销事件。remove方法通过扫描链表,寻找传入的密封了相同方法的委托来注销事件的委托。如果找到匹配的委托,就将其从事件的委托链表上移除。如果没有找到,也不会出现任何错误,事件的委托链表也不会有任何改变。
顺便说一下,C#要求代码使用+=和-=操作符在链表上添加和移除委托。如果我们试图显式地调用add和remove方法,那么C#编译器将生成一个错误消息:CS0571: “不能显式调用操作符或访问器方法”。

C# 编译器为事件的add和remove方法增加[MethodImpl (MethodImplOptions.Synchronized)]属性。这个属性的目的是为了确保在操作实例的事件成员时,对于任何一个对象,在同一时刻只能有一个add方法或者remove方法可以执行。该属性同样确保在操作静态事件成员时,同一时刻也只能有一个add方法或者remove方法可以执行。这里需要线程同步,以免委托对象的链表被破坏。但应注意,CLR实现线程同步的方式中存在许多问题。

对实例(非静态)方法应用 MethodImpl属性时,CLR使用对象本身作为线程同步锁。这意味着如果类定义了许多事件,那么所有的add和remove方法都将使用相同的锁,这种情况下,如果有多个线程同时订阅和注销不同的事件,则会损害可扩展性。但这种情况非常少见,而且对于大多数应用程序,这并不是一个问题。但是,线程同步的指导方针中规定方法不应在对象本身上加同步锁,因为同步锁将对所有的代码公开。这意味着任何人都可以编写代码锁住这个对象,从而可能导致其他线程死锁。如果希望自己编写的类型防御措施好,更加健壮,那么应使用不同的对象来完成加锁功能。10.5节将示范如何实现这个                功能。

在静态方法上应用[MethodImpl(MethodImplOptions.Synchronized)]属性时,CLR使用类型对象作为线程同步锁。这又意味着如果类定义了许多静态事件,那么所有的add和remove方法都将使用相同的锁,这种情况下,如果有多个线程同时订阅和注销不同的事件,则会损害代码的性能。但是这种情况也非常少见。

但还有一个严重的问题:线程同步指导方针指出,方法永远不要在类型对象上加锁,因为这个锁将对所有的代码公开。另外,当类型加载与域无关时,CLR中还有一个错误。这种情况会导致同步锁被使用该类型的所有应用程序域共享,如此一来,一个程序域中的代码会影响另一个应用程序域中运行的代码。实际中,C#编译器在实现静态add和remove方法的线程安全时应采用完全不同的方法。10.5节将讨论一种修正C#编译器这一缺陷的机制。

C#和CLR允许定义一个有一个或者多个实例 (非静态)事件成员的值类型(结构)。但必须意识到,在上述情况下,C#编译器根本不会保证线程安全。因为拆箱(unboxing)的值类型没有与其相关联的锁对象。实际上,C#编译器不为add和remove方法生成[MethodImpl (MethodImplOptions.Synchronized)]属性,因为该属性对值类型的实例方法没有效果。遗憾的是,当实例事件定义为值类型的成员时,的确没有更好的方式为它们保证线程安全。因此,建议大家尽量避免这样做。注意,在值类型中定义的静态事件(加上前面讨论的限制)可以保证线程安全,因为静态事件对类型对象(引用类型)本身加锁。但是,如果要使代码健壮,应采用10.5节讨论的机制。

有时我们会感到编译器生成的 add和remove方法不是那么理想。例如10.4节中讨论的使用MicrosoftC#编译器时遇到的所有线程安全问题。实际上,Microsoft 的C#编译器在安全编程(defensive coding)和健壮性方面永远不是最安全的。为了创建一个坚固的组件,建议经常采用本节介绍的技术,该技术可以用于解决与线程安全相关的所有问题。而且该技术同样也可以应用于其他目的。例如,显式实现add和remove方法的普遍原因就是类型定义了许多事件,而且又需要高效地进行存储。有关详情请参见 10.6节。

幸亏C#编译器以及其他许多编译器都允许开发人员显式地实现add和remove访问器方法。为了保证MailManager对象上事件的订阅和注销的线程安全,我们修改了MailManager类的代码,修改后的代码如下所示:


internal class MailManager {
    //创建一个作为线程同步锁的私有实例字段
    private readonly Object m_eventLock = new Object();

    //增加一个引用委托链表头部的私有字段
    private EventHandler<NewMailEventArgs> m_NewMail;

    //为类增加一个事件成员

    public event EventHandler<NewMailEventArgs> NewMail

    {       

           //显式实现'add'方法        

           add {   

                     / /加私有锁,并向委托链表增加一个处理程序(以'value'为参数)           

                      lock (m_eventLock)

                      { m_NewMail += value; }       

                 }     

           //显式实现'remove'方法        

           remove {           

                      //加私有锁,并从委托链表从中移除处理程序(以'value'为参数)     

                       lock (m_eventLock) { m_NewMail -= value; }    

                       }  

     }

在定义委托时,前面加上event关键字,可以保证该委托不能在外部被随意触发,两者异同:

 注册 -注销 -内部触发 -外部触发
delegate += -= Invoke Invoke
event delegate += -= Invoke 不允许
所以,event关键字有助于提高类的封装性,物理隔绝代码耦合,迫使类设计更追求高内聚。

定义一个显示消息的event并包装

public event EventHandler evt_log_handle;
protected virtual void On_evt_log_handle(object obj, EventArgs e)
{
if (this.evt_log_handle != null)
this.evt_log_handle(obj, e);
}
在外部触发:

On_evt_log_handle("日志", null);

在其他类中,可以注册这个事件

m_project.evt_log_handle += m_process_evt_log_handle;  m_project为定义event的类的实例

定义事件实现的代码

void m_process_evt_log_handle(object sender, EventArgs e)
{
string sLog = sender as string;
...
}
 控制台输入'a'累加到上限实现事件的示例

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp2
{
  class Program
  {
    static void Main(string[] args)
     {
      Counter c = new Counter(new Random().Next(10));
      c.ThresholdReached += c_ThresholdReached; //注册事件

      Console.WriteLine("press 'a' key to increase total");
      while (Console.ReadKey(true).KeyChar == 'a')
      {
          Console.WriteLine("adding one");
          c.Add(1);
      }
    }

    static void c_ThresholdReached(object sender, ThresholdReachedEventArgs e) //事件实现
    {
      Console.WriteLine("The threshold of {0} was reached at {1}.", e.Threshold, e.TimeReached);
      Environment.Exit(0);
    }
  }

  class Counter
  {
    private int threshold;
    private int total;

    public Counter(int passedThreshold)
    {
      threshold = passedThreshold;
    }

    public void Add(int x)
    {
      total += x;
      if (total >= threshold)
      {
        ThresholdReachedEventArgs args = new ThresholdReachedEventArgs();
        args.Threshold = threshold;
        args.TimeReached = DateTime.Now;
        OnThresholdReached(args);
      }
    }

    public event EventHandler<ThresholdReachedEventArgs> ThresholdReached;
    protected virtual void OnThresholdReached(ThresholdReachedEventArgs e)
    {
      if (this.ThresholdReached != null)
      {
        this.ThresholdReached(null, e);
      }
    }
  }

  public class ThresholdReachedEventArgs : EventArgs
  {
    public int Threshold { get; set; }
    public DateTime TimeReached { get; set; }
  }

}
 


推荐阅读