返回

事件

定义了事件成员的类型允许 类型或类型实例 通知其他对象发生了特定事情.

例如: Button按钮类提供了Click事件.应用程序中的其他对象可以接收关于该事件的通知,以便在Button被点击之后采取特定的操作.

定义了事件成员的类型能提供一下功能:

  • 方法能登记它对事件的关注.
  • 方法能注销它对事件的关注.
  • 事件发生时,登记了的方法将收到通知.

类型之所以能提供事件通知功能,是因为类型维护了一个已登记方法的列表.事件发生后通知列表中的所有已登记方法.

CLR事件模型是以委托为基础. 委托是调用回调方法的一种类型安全的方式.对象凭借回调方法接收它们订阅的通知.

我们可以通过为事件定义事件访问器,来控制事件运算符+=、-=运算符的行为,有两个访问器:add和remove,声明事件的访问器看上去和声明一个属性差不多.

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

举例用到事件的场景

设计电子邮件应用程序:

功能:电子邮件到达时,将该邮件转发给传真机,将该邮件转给寻呼机.

  1. 先构建一个MailManager的一个实例. MailManager提供了NewMail事件.
  2. 构造Fax和Pager对象时,它们向MailManager的NewMail事件登记它们自己的一个实例方法.
  3. MailManager收到新邮件时,会引发NewMail事件,使所有已登记的方法都能用自己的方式处理邮件.

第一步:定义类型来容纳所有需要发送给事件通知接收者的附加信息

事件引发时,引发事件的对象可能希望向接收事件通知的对象传递一些附加信息,这些附加信息要封装到它自己的类中, 这种类应该从EventArgs派生,并且类名以EventArgs结尾.

public  class NewMaillEventArgs : EventArgs
{
       // 私有只读字段
       // 发件人
       private readonly string _m_from;
       // 收件人
       private readonly string _m_to;
       // 主题
       private readonly string _m_subject;
       public NewMaillEventArgs(string from,string to,string subject)
       {
           _m_from = from;
           _m_to = to;
           _m_subject = subject;
       }
       // 公开的只读属性(只有get访问器)
       public string From { get { return _m_from; } }
       public string To { get { return _m_to; } }
       public string Subject { get { return _m_subject; } }
}

EventArgs类的定义实现

实现非常简单,就是一个让其他类型继承的基类型.

如果事件不需要传递附加信息,则可以直接使用EventArgs.Empty,不用构造新的EventArgs对象. 例如:Button点击事件,调用回调方法就可以了,不需要传递附加信息.

[Serializable]
public class EventArgs
{
  public static readonly EventArgs Empty = new EventArgs();
}

第二步:定义事件成员

事件成员使用C#关键字event来定义。

每个事件成员都要指定以下内容:

  • 一个可访问性标识符(几乎肯定是Public,这样其他代码才能访问该事件成员);
  • 另一个委托类型,它指出要调用的方法的原型,以及一个名称(可以是任意有效的标识符)。
//定义成员变量 NewMaill是事件名称
public event EventHandler<NewMaillEventArgs> NewMaill;

事件成员类型是EventHandler, 意味着所有接收者都必须提供一个原型和EventHandler匹配的回调方法.

由于泛型System.EventHandler的委托定义如下:

public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e);

所以方法的原型必须具有以下形式:

void MethodName(Object sender, NewMaillEventArgs e);

事件模式要求所有事件处理程序(方法)的返回类型都是void; 因为引发事件后可能要调用好几个回调方法,但是没办法获得所有方法的返回值,将返回类型void,就不允许回调方法返回值. 但是ResloveEventHandle事件没有遵循这个要求,返回了Assembly类型的对象.

为什么要求sender参数是Object类型

只有MailManager对象才会引发传递NewMaillEventArgs对象的事件,似乎以下回调方法更适合原型:

// Object sender 改为 MailManager sender
void MethodName(MailManager sender, NewMaillEventArgs e);

原因是:

  1. 要求Object主要是因为继承.

    假设: MailManager成为SmtpMailManager的基类SmtpMailManager : MailManager ,SmtpMailManager从基类继承了事件NewMail, 那参数类型需要由MailManager转换为SmtpMailManager,反正都是需要转换的,不如定义成Object.

  2. 另一个原因是灵活性,使委托能由多个类型使用.

只要类型提供一个会传递NewMaillEventArgs对象的事件,即使PopMailManager类不从MailManager类派生,也能使用这个委托.

第三步:定义负责引发事件的方法来通知事件的登记对象

事件主要是在单线程情形中使用,所以线程安全不是问题.

//如果类是密封的,该方法要声明为私有和非虚
protected virtual void OnNewMaill(NewMaillEventArgs e)
{
    //出于线程安全的考虑,现在将对委托字段的引用复制到一个临时变量时
    // EventHandler<NewMaillEventArgs> temp = NewMaill; 也是可以的
    // 事件主要是在单线程的情形中使用,所以线程安全不是问题
    // (NewMaill != null) 在多线程中会出现竞态问题
    // 以下写法是技术正确的版本.
    EventHandler<NewMaillEventArgs> temp = Volatile.Read(ref NewMaill);
    //任何方法登记了事件的关注,就通知它们
    if (temp != null)
    {
        temp(this, e);
    }
}

(NewMaill != null) 在多线程中会出现竞态问题,调用NewMaill之前,在另外一个线程中可能移除一个委托,使NewMaill成了null. 许多开发者就像EventHandler temp = NewMaill;这样写, 将NewMail引用复制到变量中,引用的是赋值时发生的委托链. 委托是不可变的.

将MailManager作为基类,派生类可以自由重写OnNewMaill方法,一般情况下,派生类会调用基类的OnNewMaill方法,使登记的方法都能收到通知,但是派生类也可以不允许事件转发.

定义如下扩展方法来封装这个线程安全逻辑

public static class EventArgExtensions
{
    public static void Raise<TEventargs>(this TEventargs e, Object sender, ref EventHandler<TEventargs> eventHandler)
    {
        EventHandler<TEventargs> temp = Volatile.Read(ref eventHandler);

        if (temp != null)
        {
            temp(sender, e);
        }
    }
}

为了方便起见,就可以如下重写OnNewMail方法

protected virtual void OnNewMaill(NewMaillEventArgs e)
{
  e.Raise(this, ref m_NewMail);
}

第四步:定义方法将输入转化为期望事件

public void SimulateNewMaill(String from, String to, String subject)
{
    // 构造对象来容纳想附加给接收者的信息
    NewMaillEventArgs e =new NewMaillEventArgs(from,to,subject);

    // 调用虚方法通知事件已经发生
    // 如果没有类型重写该方法,我们的对象将通知事件的所有登记对象
    OnNewMaill(e);
}

编译器如何实现事件

// 事件成员
public EventHandler<NewMaillEventArgs> NewMail = null;

C#编译器编译时把它转换为3个构造:

  1. 一个被初始化为null的私有委托字段;

    是对委托列表头部的引用.

  2. 一个公共的add_Xxx方法(Xxx是事件名,例如add_NewMail)

    生成的代码总是调用System.Delgate的静态Combine方法,将委托实例添加到委托列表中,返回新的列表头地址, 将这个地址存回字段.

  3. 一个公共的remove_Xxx(Xxx是事件名,例如remove_NewMail)

    生成的代码总是调用System.Delgate的静态Remove方法,将委托实例从委托列表中删除中,返回新的列表头地址, 将这个地址存回字段.

如果试图删除从未添加过的方法, Delgate内部不做任何事情,也不会抛出异常, 事件的集合保持不变.

add和remove以线程安全的一种模式更新值.

事件的可访问性决定了什么代码能登记和注销对事件的关注.无论如何只有类本身可以访问委托字段.

设计侦听事件的类型

  1. 在Fax类中的构造函数传入MailManager,并将对该对象的引用保存到变量中,
  2. 在构造函数中登记它对MailManager的事件的关注
    1. mm.NewMail += FaxMsg;
    2. C#编译器对+=操作符翻译成以下代码来添加对象对事件的关注.mm.add_NewMail(new EventHandler(this.Msg));
    3. C#编译器调用了MailManager类的add_NewMail方法,传递新的委托对象(Msg).
  3. MailManager对象引发事件时, Fax对象的FaxMsg方法会被调用.
    1. 调用时,传递的第一个参数时MailManager对象引用,sender. 可以用来访问MailManager对象的成员.
    2. NewMaillEventArgs附带了信息.
  4. 类型实现了IDisposable的Dispose方法,就应该在其中注销对所有事件的关注. 用-=操作符(会调用remove方法).

C#要求代码使用+=和-=操作符在列表中增删委托.

什么情况需要显式实现事件?

在一个类型中定义了很多事件,也就是要很多个委托字段,但是用到的只是少数事件, 这样从这个类派生创建的对象都要浪费大量内存.

因此需要开发人员显式实现一个事件,使开发人员能够控制add和remove方法处理回调委托.

  1. 高效率存储事件委托, 公开了事件的每个对象都要维护一个集合(通常是字典)

    事件标识符作为key, 委托列表作为值value

    新对象构造时, 集合是空白的, 登记事件时会查找集合中的键(事件标识符),如果已经有了,就将新委托和委托列表合并. 不存在就新添加.

  2. 所以这个设计,需要定义事件的那个类型开发人员去完成.

EventKey和EventSet设计

using System;
using System.Collections.Generic;
using System.Threading;

// 此类目的:提供多一点的类型安全性和代码的可维护性
// 用于 字典的key键存放每个事件标识符类的哈希码,以便查找这个事件的委托链表
public sealed class EventKey : Object
{
}

// 用于显示实现事件的字典集合
public sealed class EventSet
{
    // 定义私有只读字典,用于维护EventKey->Delegate(委托链表)映射
    private readonly Dictionary<EventKey, Delegate> m_events =
        new Dictionary<EventKey, Delegate>();

    // Adds an EventKey -> Delegate mapping if it doesn't exist or
    // combines a delegate to an existing EventKey
    public void Add(EventKey eventKey, Delegate handler)
    {
        // 线程安全的方式
        Monitor.Enter(m_events);
        Delegate d;
        m_events.TryGetValue(eventKey, out d);
        // 合并新的委托到委托链表中
        // Delegate.Combine将`委托实例添加到委托列表`中,返回`新的列表头地址`, 将这个地址存回字段.
        m_events[eventKey] = Delegate.Combine(d, handler);
        Monitor.Exit(m_events);
    }

    // Removes a delegate from an EventKey (if it exists) and
    // removes the EventKey -> Delegate mapping the last delegate is removed
    public void Remove(EventKey eventKey, Delegate handler)
    {
        Monitor.Enter(m_events);
        // Call TryGetValue to ensure that an exception is not thrown if
        // attempting to remove a delegate from an EventKey not in the set
        Delegate d;
        // 确保从集合中删除不存在的EventKey时不会抛出异常.
        if (m_events.TryGetValue(eventKey, out d))
        {
            d = Delegate.Remove(d, handler);

            // 如果委托链表还存在委托,则重新设置头部的地址.
            if (d != null) m_events[eventKey] = d;
            // 链表为空,则移除这个键值对.
            else m_events.Remove(eventKey);
        }

        Monitor.Exit(m_events);
    }

    // 为指定的EventKey引发事件
    public void Raise(EventKey eventKey, Object sender, EventArgs e)
    {
        // 如果EventKey不在集合中,不会抛出异常
        Delegate d;
        Monitor.Enter(m_events);
        m_events.TryGetValue(eventKey, out d);
        Monitor.Exit(m_events);

        if (d != null)
        {
            // 由于字典可能包含几个不同的类型委托
            // 所以无法再编译时构造一个类型安全的委托调用
            // 调用Delegate的DynamicInvoke方法,以一个对象数组的形式传递回调方法的参数
            // 在内部,DynamicInvoke会向调用的回调方法查证参数的类型安全性.
            // 如果类型不匹配,会抛出异常
            d.DynamicInvoke(new Object[] {sender, e});
        }
    }
}

在TypeWithLotsOfEvents类中使用EventSet类

TypeWithLotsOfEvents类(使用了大量时间的类):

using System;

// 为事件定义从EventArgs派生的附加信息类
public class FooEventArgs : EventArgs
{
}

// Define the EventArgs-derived type for this event.
public class BarEventArgs : EventArgs
{
}

///////////////////////////////////////////////////////////////////////////////

internal class TypeWithLotsOfEvents
{
    // 定义私有只读字段来引用集合类
    // 用于管理维护一组EventKey->Delegate(事件/委托)对.
    private readonly EventSet m_eventSet = new EventSet();

    // protected只能派生类型能访问集合
    protected EventSet EventSet
    {
        get { return m_eventSet; }
    }

    // 定义Foo事件的键
    // 2a. 构造静态只读对象来作为标识符表示这个事件
    // 每个对象都有自己的哈希码,可以方便在对象的集合中查找这个事件的委托链表
    protected static readonly EventKey s_fooEventKey = new EventKey();

    // 2d. 定义事件的访问器, 用于在集合中增删委托
    public event EventHandler<FooEventArgs> Foo
    {
        // 操作EventSet集合来 增加/删除 键值对
        add { m_eventSet.Add(s_fooEventKey, value); }
        remove { m_eventSet.Remove(s_fooEventKey, value); }
    }

    // 2e. 定义虚方法,用来触发事件
    protected virtual void OnFoo(FooEventArgs e)
    {
        m_eventSet.Raise(s_fooEventKey, this, e);
    }

    // 2f. 定义将输入转成这个事件的方法
    public void SimulateFoo()
    {
        OnFoo(new FooEventArgs());
    }
}

使用TypeWithLotsOfEvents类型

public static void Main()
{
  TypeWithLotsOfEvents twle = new TypeWithLotsOfEvents();
  // 添加一个回调
  twle.Foo += HandleFooEvent;
  // 证明确实可行
  twle.SimulateFoo();
}

private static void HandleFooEvent(object sender, FooEventArgs e)
{
  Console.WriteLine("Handling Foo Event here..");
}
Licensed under CC BY-NC-SA 4.0
0