《CLR Via C# 第3版》笔记之(十二) – 事件

熟悉C#中的事件机制,使得大家可以编写出尤其靠近于实际景况的顺序。

重中之重内容:

  • 本例中事件的情景介绍
  • 事件的协会
  • 登记/注销事件
  • 事件在编译器中的达成
  • 显式达成事件

1. 本例中事件的气象介绍

为了更好的牵线事件的机制,首先大家社团一个行使事件的风貌(是本身原先面试时相遇的一个编程题)。

具体处境大致是这么的:某工厂有个设备,当这几个设备的热度高达90摄氏度时,触发警报器报警,同时发送短信公告相关工作人士。

立马我就大致的结构3个类:设备(Equipment),警报器(Alert),短信装置(Message)。

历史观的落实格局:

1.
警报器类(Alert)中编辑一个报警的章程(StartAlert),短信装置类(Message)中编辑一个发短信的措施(SendMessage)

2.
在装置类(Equipment)中编辑一个热度90度时调用的法子(SimulateTemperature90)

  1. 在设备类(Equipment)中援引警报器类(Alert)和短信装置类(Message)

4.
当温度90度时,SimulateTemperature90中会调用Alert.StartAlert方法和Message.SendMessage方法来成功所需成效

依照事件的兑现格局:

1.
警报器类(Alert)中编辑一个报警的办法(StartAlert),短信装置类(Message)中编辑一个发短信的点子(SendMessage)

2.
在设施类(Equipment)中编辑一个热度90度时调用的措施(SimulateTemperature90)

  1. 在配备类(Equipment)中编辑一个事件(Temperature90)

4.
警报器类(Alert)向设施类(Equipment)注册StartAlert,短信装置类(Message)向设备类(Equipment)注册SendMessage

  1. 当温度90度时,SimulateTemperature90会触发事件(Temperature90),

   
此事件会调用已登记的Alert.StartAlert方法和Message.SendMessage方法来成功所需作用

价值观的法子缺陷:

1.
设施类(Equipment)关心警报器类(Alert)和短信装置类(Message),当警报器类(Alert)中的方法变更时,

   
除了要修改警报器类(Alert),还非得修改设备类(Equipment)中调用警报器类(Alert)中方法的地点

2.
扩充效益时,比如增添另一个报警装置(Alert2)时,须求修改设备类(Equipment)中报警的地点

根据事件的兑现形式可以很好的核查上述情形:

1.
警报器类(Alert)和短信装置类(Message)关心设备类(Equipment),当警报器类(Alert)中的方法变更时,

   
只需修改警报器类(Alert)中登记事件的地方,将新的轩然大波注册到装备类(Equipment)的事件(Temperature90)中即可

2.
增加效益时,比如伸张另一个报警装置(Alert2)时,将新的报警格局注册到事件(Temperature90)中即可

3.
注销警报器类(Alert)和短信装置类(Message)事件时,只需在相应的类中打消事件就行,无需修改设备类(Equipment)

2. 事变的构造

上边以事件的格局来布局上述应用。

  1. 编纂事件传递的参数,暂时只含有设备名

  2. 概念事件成员

  3. 概念触发事件的章程

  4. 概念模拟触发事件的艺术

代码如下:

// 编写事件传递的参数,暂时只包含设备名
public class EquipmentEventArgs:EventArgs
{
    // 设备名
    private readonly string equipName;
    public string EquipName { get { return equipName; } }

    public EquipmentEventArgs(string en)
    {
        equipName = en;
    }
}

public class Equipment
{
    // 设备名
    private readonly string equipName;
    public string EquipName { get { return equipName; } }

    public Equipment(string en)
    {
        equipName = en;
    }

    // 定义事件成员
    public event EventHandler<EquipmentEventArgs> Temperature90;

    // 定义触发事件的方法
    protected void OnTemperature90(EquipmentEventArgs e)
    {
        Temperature90(this, e);
    }

    // 定义模拟触发事件的方法
    public void SimulateTemperature90()
    {
        // 事件参数的初始化
        EquipmentEventArgs e = new EquipmentEventArgs(this.EquipName);
        // 触发事件
        OnTemperature90(e);
    }
}

3. 报了名/注销事件

概念警报器类(Alert)和短信装置类(Message),并在其中落到实处登记/注销事件的主意。

代码如下:

// 警报器类
public class Alert
{
    // 定义要注册的函数,注意此函数的签名是与 EventHandler<EquipmentEventArgs>一致的
    private void StartAlert(Object sender, EquipmentEventArgs e)
    {
        Console.WriteLine("Equipment: " + e.EquipName + "'s temperature is 90 now!");
    }

    // 向Equipment注册事件
    public void Register(Equipment equip)
    {
        equip.Temperature90 += StartAlert;
    }

    // 向Equipment注销事件
    public void UnRegister(Equipment equip)
    {
        equip.Temperature90 -= StartAlert;
    }
}

// 短信装置类
public class Message
{
    // 定义要注册的函数,注意此函数的签名是与 EventHandler<EquipmentEventArgs>一致的
    private void SendMessage(Object sender, EquipmentEventArgs e)
    {
        Console.WriteLine("Equipment: " + e.EquipName + " sends ‘temperature is 90 now!’ to administrator");
    }

    // 向Equipment注册事件
    public void Register(Equipment equip)
    {
        equip.Temperature90 += SendMessage;
    }

    // 向Equipment注销事件
    public void UnRegister(Equipment equip)
    {
        equip.Temperature90 -= SendMessage;
    }
}

调用上述事件的代码如下:

class CLRviaCSharp_12
{
    static void Main(string[] args)
    {
        Equipment eq = new Equipment("My Equipment");
        Alert alert = new Alert();
        Message msg = new Message();

        // 注册Alert和Message的事件后
        Console.WriteLine("=========注册Alert和Message的事件后=================");
        alert.Register(eq);
        msg.Register(eq);
        eq.SimulateTemperature90();

        // 注销Alert的事件后
        Console.WriteLine();
        Console.WriteLine("=========注销Alert的事件后,只有Message事件==========");
        alert.UnRegister(eq);
        eq.SimulateTemperature90();

        Console.ReadKey(true);
    }
}

调用结果如下:

图片 1

4. 事变在编译器中的达成

在事件的挂号/注销时,大家无非是简单利用
+=和-=。那么编译其是哪些注册/注销事件的啊。

原本编译器在编译时会自动按照大家定义的共用事件(public event
伊夫ntHandler<Equipment伊夫ntArgs> Temperature90)

生成私有字段Temperature90和事件的add和remove方法。通过ILSpy查看上边编译出的程序集,如下图:

图片 2

动用 +=和-=时,就是调用编译器生成的add_***和remove_***方法。

下边就是Alert类的Registered和UnRegister方法的IL代码。

图片 3

事件是引用类型,那么事件在拓展注册或者吊销时会不会设有线程并发的难点。比如四个线程同时向设施类(Equipment)注册或打消事件时,会不会油不过生挂号或注销不成功的状态吧。

咱俩更加观望add_Temperature90的IL代码如下

图片 4

最首要部分就是上图中的黄色线框部分,可能有些人不太熟稔IL代码,我将方面的代码翻译成C#约莫如下:

public void add_Temperature90(EventHandler<EquipmentEventArgs> value)
{
    //[0] class [mscorlib]System.EventHandler`1<class cnblog_bowen.EquipmentEventArgs>            
    EventHandler<EquipmentEventArgs> args0;
    //[1] class [mscorlib]System.EventHandler`1<class cnblog_bowen.EquipmentEventArgs>           
    EventHandler<EquipmentEventArgs> args1;
    //[2] class [mscorlib]System.EventHandler`1<class cnblog_bowen.EquipmentEventArgs>           
    EventHandler<EquipmentEventArgs> args2;
    //[3] bool          
    bool args3;

    // IL_0000 ~ IL_0006
    args0 = this.Temperature90;

    // IL_0007 ~ IL_002d
    do
    {
        // IL_0007 ~ IL_0008
        args1 = args0;

        // IL_0009 ~ IL_0015
        args2 = (EventHandler<EquipmentEventArgs>)System.Delegate.Combine(args1, value);

        // IL_0016 ~ IL_0023
        args0 = System.Threading.Interlocked.CompareExchange<EventHandler<EquipmentEventArgs>>(ref this.Temperature90, args2, args1);

        // IL_0024 ~ IL_002d
        if (args0 != args1)
            args3 = true;
        else
            args3 = false;
    }
    while (args3);
}

从地方代码可以见见,添加新的事件后(IL_0009 ~
IL_0015
),IL会继续表明原有的事件是还是不是被其余线程修改过( IL_0016 ~ IL_0023)

因此我们在注册/注销事件时毫不顾虑线程安全的难题。

5. 显式落成事件

从地点的IL代码,大家见到每个事件都会变动对应的民用字段和相应的add_***和remove_***方法。

对于一个有不少轩然大波的类(比如Windows.Forms.Control)来说,将会生成多量的事件代码,而大家在使用时数次只是利用其中很少的一有些事件。

那般使得在创制这几个类的时候浪费多量的内存。为了防止那种处境和高效用的存取事件委托,我们可以协会一个字典来尊敬大批量的事件。

Jeffery Richard在《CLR via
C#》中给了大家一个很好的事例,为了将来参考方便,摘抄如下:

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

// 这个类的目的是在使用EventSet时,提供
// 多一点的类型安全型和代码可维护性
public sealed class EventKey : Object { }

public sealed class EventSet
{
    // 私有字典,用于维护 EventKey -> Delegate映射
    private readonly Dictionary<EventKey, Delegate> m_events =
        new Dictionary<EventKey, Delegate>();

    // 添加一个EventKey -> Delegate映射(如果EventKey不存在),
    // 或者将一个委托与一个现在EventKey合并
    public void Add(EventKey eventKey, Delegate handler)
    {
        Monitor.Enter(m_events);
        Delegate d;
        m_events.TryGetValue(eventKey, out d);
        m_events[eventKey] = Delegate.Combine(d, handler);
        Monitor.Exit(m_events);
    }

    // 从EventKey(如果它存在)删除一个委托,并且
    // 在删除最后一个委托时删除EventKey -> Delegate映射
    public void Remove(EventKey eventKey, Delegate handler)
    {
        Monitor.Enter(m_events);
        // 调用TryGetValue,确保在尝试从集合中删除一个不存在的EventKey时,
        // 不会抛出一个异常。
        Delegate d;
        if (m_events.TryGetValue(eventKey, out d))
        {
            d = Delegate.Remove(d, handler);

            // 如果还有委托,就设置新的地址,否则删除EventKey
            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)
        {
            // 由于字典可能包含几个不同的委托类型,
            // 所以无法在编译时构造一个类型安全的委托调用。
            // 因此,我调用System.Delegate类型的DynamicInvoke方法
            // 以一个对象数组的形式向它传递回调方法的参数。
            // 在内部,DynamicInvoke会向调用的回调方法查证参数的类型安全性,并调用方法。
            // 如果存在类型不匹配的情况,DynamicInvoke会抛出一个异常。
            d.DynamicInvoke(new Object[] { sender, e });
        }
    }
}

选取伊芙ntSet类的措施也很简短,修改上边的设备类(Equipment)如下:

public class Equipment
{
    // 设备名
    private readonly string equipName;
    public string EquipName { get { return equipName; } }

    public Equipment(string en)
    {
        equipName = en;
    }

    private readonly EventSet m_events = new EventSet();

    // 用于标示事件类型的Key,当有新的事件时,需要再增加一个Key
    private static readonly EventKey m_eventkey = new EventKey();

    // 注册/注销事件
    public event EventHandler<EquipmentEventArgs> Temperature90
    {
        add { m_events.Add(m_eventkey, value); }
        remove { m_events.Remove(m_eventkey, value); }
    }

    // 定义触发事件的方法
    protected void OnTemperature90(EquipmentEventArgs e)
    {
        m_events.Raise(m_eventkey, this, e);
    }

    // 定义模拟触发事件的方法
    public void SimulateTemperature90()
    {
        // 事件参数的初始化
        EquipmentEventArgs e = new EquipmentEventArgs(this.EquipName);
        // 触发事件
        OnTemperature90(e);
    }
}

此外代码不用修改,编译后可正常运作。

伊芙ntSet类是对准事件很多的类来设计的,本例惟有一个轩然大波,那样做的优势并不肯定。

相关文章