zoukankan      html  css  js  c++  java
  • 《CLR Via C# 第3版》笔记之(十二) 事件

    熟悉C#中的事件机制,使得我们可以编写出更加贴近于实际情况的程序。

    主要内容:

    • 本例中事件的场景介绍
    • 事件的构造
    • 注册/注销事件
    • 事件在编译器中的实现
    • 显式实现事件

    1. 本例中事件的场景介绍

    为了更好的介绍事件的机制,首先我们构造一个使用事件的场景(是我以前面试时遇到的一个编程题)。

    具体场景大概是这样的:某工厂有个设备,当这个设备的温度达到90摄氏度时,触发警报器报警,同时发送短信通知相关工作人员。

    当时我就简单的构造3个类:设备(Equipment),警报器(Alert),短信装置(Message)。

    传统的实现方法:

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

    2. 在设备类(Equipment)中编写一个温度90度时调用的方法(SimulateTemperature90)

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

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

    基于事件的实现方法:

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

    2. 在设备类(Equipment)中编写一个温度90度时调用的方法(SimulateTemperature90)

    3. 在设备类(Equipment)中编写一个事件(Temperature90)

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

    5. 当温度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);
        }
    }

    调用结果如下:

    image

    4. 事件在编译器中的实现

    在事件的注册/注销时,我们仅仅是简单使用 +=和-=。那么编译其是如何注册/注销事件的呢。

    原来编译器在编译时会自动根据我们定义的公共事件(public event EventHandler<EquipmentEventArgs> Temperature90)

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

    image

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

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

    image

    事件是引用类型,那么事件在进行注册或者注销时会不会存在线程并发的问题。比如多个线程同时向设备类(Equipment)注册或注销事件时,会不会出现注册或注销不成功的情况呢。

    我们进一步观察add_Temperature90的IL代码如下

    image

    主要部分就是上图中的红色线框部分,可能有些人不太熟悉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 });
            }
        }
    }

    使用EventSet类的方法也很简单,修改上面的设备类(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);
        }
    }

    其余代码不用修改,编译后可正常运行。

    EventSet类是针对事件很多的类来设计的,本例只有一个事件,这样做的优势并不明显。

  • 相关阅读:
    前端页面获取各类页面尺寸及坐标尺寸总结
    禁止微信内置浏览器调整字体大小
    区分浏览器,判断浏览器版本
    JavaScript
    ASP.NET MVC,Entity Framework 及 Code First
    循序渐进MongoDB V3.4(Ubuntu)
    Webpack
    RequireJS Step by Step
    JavaScript Object 及相关操作
    ES6 Promises
  • 原文地址:https://www.cnblogs.com/wang_yb/p/2103058.html
Copyright © 2011-2022 走看看