• 事件和委托


    以一个列子开始了解委托更容易些,下面是委托的使用方式:

    internal delegate void Feedback(int32 value)fb;
    //使用
    public void main()
    {
        fb = getsomthing;
        fb+=getanything;
         
        if(fb != null)
        {
            fb(321);
        }
        fb-=getanything;
        fb(123);
    }
    
    private void getsomthing(int32 value)
    {
        Console.WriteLine($"this value is {value}")
    }
    
    private static void getanything(int32 value)
    {
        Console.WriteLine($"this value is anything {value}")
    }

    编译器在遇到 :internal delegate void Feedback (int32 value);语句时会将它编译为一个类,这个类继承 System.MulticastDelegate 基类,而System.MulticastDelegate 又继承自 System.delegate。

    这个类也有构造函数,它要求传递两个参数,一个 object 和一个 IntPtr 。把它们保存到内部的字段中(字段是来自基类),以后会通过它们找到回调函数。

    这个类其他的方法有,invoke、BeginIvoke,EndIvoke。这个具体是啥样参照下面的代码:

    internal class Feedback : System.MulticastDelegate {
        // Constructor
        public Feedback(Object @object, IntPtr method);
        
        // Method with same prototype as specified by the source code
        public virtual void Invoke(Int32 value);
        
        // Methods allowing the callback to be called asynchronously
        public virtual IAsyncResult BeginInvoke(Int32 value,
            AsyncCallback callback, Object @object);
        
        public virtual void EndInvoke(IAsyncResult result);
    }

    它的基类MulticastDelegate 有三个关键字段:

     

    • target 是一个 Object 类型,如果包装的方法是静态的,这个字段就是null,如果包装的实例方法,这个字段就是实例的引用。

    • methodPtr 是一个内部整数值,CLR 用它标识回调方法。

    • _invocationList 是Object 类型,构造委托链时它引用一个委托数组。

    上面的C# 语句 fb = getsomthing;,实际上会被编译器转为 Feedback fb = new Feedback(getsomthing);

    在委托调用回调方法时:fb(321),实际上是调用 委托实例的 invoke 方法,这个方法的返回值和参数与 getsomthing 一致,它使用私有变量 _target 和 _methodPtr 在指定对象上调回调方法。

     

    委托链

    基类 Delegate 中有两个重要的方法,一个是Combine 另一个是 Remove,是构造委托链时使用的。

    上面的语句 fb+=getanything; 编译器会转为fb = (Feedback)Delegate.Combine(fb , new Feedback(getanything));

    Combine 方法传递两个参数,一个是fb ,另一个 是新的委托实例。分三种情况:

    • 如果fb 是null,Combine 直接返回 参数2。

    • 如果fb 不是null,Combine 创建一个新的委托实例,并将_invocationList 初始化为一个委托数组,第一个元素是参数1,第二个元素是参数2.

    • 如果fb 不是null,第一个参数也是一个包含委托数组的委托实例,Combine也会创建一个新的委托实例,并将它的 _invocationList 初始化成一个委托数组,元素的前几个是参数1的委托数组元素,最后是参数2

      

    图是CLR via C# 中截取的,fb被赋值新的委托实例后,原来的就被垃圾回收处理了。

     

    当C# 遇到fb-=getanything; C#编译器会转为这样:fb = (Feedback)Delegate.Remove(fb , new Feedback(getanything));

    Remove 方法传递也是两个参数,Remove 方法的内部有以下几个步骤:

    • 首先扫描参数1 的委托数组,从尾到头。

    • 如果委托数组中有参数2,就删除它,

      • 删除后如果数组为空,返回null,

      • 如果只剩一个,返回这个委托实例,

      • 如果还剩多个,就再创建一个新的委托实例,将参数1的数组除去参数2,全部赋值到新委托实例中的数组中。

     

    在调用 fb(321) 时会触发这个委托链。委托的invoke 方法是定义 Feedback 类型时就定义好了。下面是伪代码实现 invoke方法:

    public void Invoke(Int32 value) {
        Delegate[] delegateSet = _invocationList as Delegate[];
        if (delegateSet != null) {
            // This delegate's array indicates the delegates that should be called
            foreach (Feedback d in delegateSet)
                d(value); // Call each delegate
        } else {
            // This delegate identifies a single method to be called back
            // Call the callback method on the specified target object.
            _methodPtr.Invoke(_target, value);
            // The preceding line is an approximation of the actual code.
            // What really happens cannot be expressed in C#.
        }
    }
    ​
    //如果是有返回值的回调方法,invoke 也被定义为有返回值,但是委托链调用后,只返回最后一个委托实例包装方法的返回值。
    //这是为啥呢?看下面的 invoke方法的实现就知道了。不是我臆想的,它截取自 CLR via C#。
    public Int32 Invoke(Int32 value) {
        Int32 result;
        Delegate[] delegateSet = _invocationList as Delegate[];
        if (delegateSet != null) {
            // This delegate's array indicates the delegates that should be called
            foreach (Feedback d in delegateSet)
                result = d(value); // Call each delegate
        } else {
            // This delegate identifies a single method to be called back
            // Call the callback method on the specified target object.
            result = _methodPtr.Invoke(_target, value);
            // The preceding line is an approximation of the actual code.
            // What really happens cannot be expressed in C#.
        }
        return result;
    }

    调用委托链时是顺序执行委托函数的,如果某个委托实例包含的方法运行出错了,上面的这种方法会导致,接下来的委托实例包含的方法得不到调用。

    怎么办呢? 解决办法是从fb 实例中获取出委托数组。

    MulticastDelegate 类提供了一个实例方法 GetInvocationList,用于显示调用委托数组中的每个委托。

    public abstract class MulticastDelegate : Delegate {
        // Creates a delegate array where each element refers
        // to a delegate in the chain.
        public sealed override Delegate[] GetInvocationList();
    }

    使用的方式如下:

     事件

    事件也是类型中的一种成员。
     
    什么叫事件?
     
    定义了事件成员的类型,允许类型或者类型的实例 通知其他对象发生了特定的事情。 
    具体的说 定义了事件成员的类型 能提供以下功能:
     
    1 方法能登记他对事件的关注
    2 方法能注销他对事件的关注
    3 事件发生时,登记了的方法将收到通知
     
    事件通知功能是怎么做到的?
     
    因为类型中维护了一个列表,列表中记录了已登记的方法。事件发生后类型将通知列表中所有的登记方法。 
    CLR事件模型以委托为基础,对象凭借回调方法接收他们订阅的通知。
     
    实现一个事件需要有哪些操作?

    1、定义一个事件类型,  事件引发时,引发事件的对象,希望能向接收事件的对象发送一些附加信息。这些附加信息封装在自己的类中(可以定义字段、属性来实现)。

    这种类应该从System.EventArgs 派生, 而且类名以 EventArgs 结束。

    // Step #1: Define a type that will hold any additional information that
    // should be sent to receivers of the event notification
    internal class NewMailEventArgs : EventArgs {
        private readonly String m_from, m_to, m_subject;
        public NewMailEventArgs(String from, String to, String subject) {
            m_from = from; m_to = to; m_subject = subject;
        }
        public String From { get { return m_from; } }
        public String To { get { return m_to; } }
        public String Subject { get { return m_subject; } }
    }

    2、定义事件成员

    事件成员使用C#关键字 event 定义。每个事件成员都要指定以下内容:    
    • 可访问性标识符 public 、 委托类型、名称;    
    • 用第一步定义好的事件类型,定义一个事件成员。
    internal class MailManager {
        // Step #2: Define the event member
        public event EventHandler<NewMailEventArgs> NewMail;
        ...
    }

    3、定义负责引发事件的方法

        按照约定,类要定义一个受保护的虚方法。引发事件时,类及派生类中的代码会调用该方法。这个方法只获取一个参数,就是第一步定义的事件类型实例的对象,它包含了要发送的消息。在这个方法给事件订阅者发送事件消息之前,会检查一下是否有人订阅了该事件,如果有的话再发送。在执行发送事件消息的时候,涉及到多线程并发的问题,确保一个事件不能被重发两次。
    protected virtual void OnNewMail(NewMailEventArgs e) {
        EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
        if (temp != null) temp(this, e);
    }

    像上面这样写没有问题,但是用扩展泛型方法可以让所有的事件类型都能用该函数,只要它的参数是一个 EventHandler委托。

    public static class EventArgExtensions {
        public static void Raise<TEventArgs>(this TEventArgs e,
        Object sender, ref EventHandler<TEventArgs> eventDelegate) {
            // Copy a reference to the delegate field now into a temporary field for thread safety
            EventHandler<TEventArgs> temp = Volatile.Read(ref eventDelegate);
            // If any methods registered interest with our event, notify them
            if (temp != null) temp(sender, e);
        }
    }

    使用的时候如下所示:

    protected virtual void OnNewMail(NewMailEventArgs e) {
        e.Raise(this, ref m_NewMail);
    }

    4、实例化一个事件对象,并调用扩展方法,将事件发送出去,通过触发委托的方式。

    internal class MailManager {
        // Step #4: Define a method that translates the
        // input into the desired event
        public void SimulateNewMail(String from, String to, String subject) {
        // Construct an object to hold the information we want
        // to pass to the receivers of our notification ; 实例化一个 事件对象,它包含了事件该有的信息。
        NewMailEventArgs e = new NewMailEventArgs(from, to, subject);
        // Call our virtual method notifying our object that the event
        // occurred. If no type overrides this method, our object will
        // notify all the objects that registered interest in the event
        OnNewMail(e);
        }
    }
     
    编译器是如何实现事件的,事件是如何工作的?
     
    MailManager类用一行代码的定义了事件成员本身。C#编译器将它转换为以下三个构造。下面的代码中,CLR via C# 作者遇到并发问题都是用Interlocked.CompareExchange。
     
    // 1. A PRIVATE delegate field that is initialized to null
    private EventHandler<NewMailEventArgs> NewMail = null;
    
    // 2. A PUBLIC add_Xxx method (where Xxx is the Event name)
    // Allows methods to register interest in the event.
    public void add_NewMail(EventHandler<NewMailEventArgs> value) {
        // The loop and the call to CompareExchange is all just a fancy way
        // of adding a delegate to the event in a thread-safe way
        EventHandler<NewMailEventArgs>prevHandler;
        EventHandler<NewMailEventArgs> newMail = this.NewMail;
        do {
            prevHandler = newMail;
            EventHandler<NewMailEventArgs> newHandler =
            (EventHandler<NewMailEventArgs>) Delegate.Combine(prevHandler, value);
            newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(
            ref this.NewMail, newHandler, prevHandler);
        } while (newMail != prevHandler);
    }
    // 3. A PUBLIC remove_Xxx method (where Xxx is the Event name)
    // Allows methods to unregister interest in the event.
    public void remove_NewMail(EventHandler<NewMailEventArgs> value) {
        // The loop and the call to CompareExchange is all just a fancy way
        // of removing a delegate from the event in a thread­safe way
        EventHandler<NewMailEventArgs> prevHandler;
        EventHandler<NewMailEventArgs> newMail = this.NewMail;
        do {
            prevHandler = newMail;
            EventHandler<NewMailEventArgs> newHandler =
            (EventHandler<NewMailEventArgs>) Delegate.Remove(prevHandler, value);
         newMail = Interlocked.CompareExchange<EventHandler<NewMailEventArgs>>(ref this.NewMail, newHandler, prevHandler);
      } while (newMail != prevHandler); 
    }  

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

    private EventHandler<NewMailEventArgs> NewMail = null;
     
    2 一个公共 add_Xxx 方法 , Xxx 是事件名。用来记录 方法登记对事件的关注。
     
    3 一个Remove_Xxx 方法, Xxx 是事件名,用来记录 方法注销对事件的关注。
     
    编译器还会再托管程序集的元数据中生成一个事件定义记录项。这个记录项包含了一些标志, 和一些基础委托类型,还引用了add 和 Remove 访问器方法, 建立事件的抽象概念,和它的访问器方法之间的联系。
     
    设计侦听事件的类型
     
    这里就比较简单了,在接收事件消息的类中定义一个符合委托类型的方法,将它记录到事件的回调方法中。
    internal sealed class Fax {
        // Pass the MailManager object to the constructor
        public Fax(MailManager mm) {
            // Construct an instance of the EventHandler<NewMailEventArgs>
            // delegate that refers to our FaxMsg callback method.
            // Register our callback with MailManager's NewMail event
            mm.NewMail += FaxMsg;
        }
        // This is the method the MailManager will call
        // when a new email message arrives
        private void FaxMsg(Object sender, NewMailEventArgs e) { 
            // 'sender' identifies the MailManager object in case
            // we want to communicate back to it.
            // 'e' identifies the additional event information
            // the MailManager wants to give us.
            // Normally, the code here would fax the email message.
            // This test implementation displays the info in the console
            Console.WriteLine("Faxing mail message:");
            Console.WriteLine(" From={0}, To={1}, Subject={2}",e.From, e.To, e.Subject);
        }
        // This method could be executed to have the Fax object unregister
        // itself with the NewMail event so that it no longer receives
        // notifications
        public void Unregister(MailManager mm) {
            // Unregister with MailManager's NewMail event
            mm.NewMail -= FaxMsg;
        }
    }
    显式实现事件
     
    为什么要显式实现事件?
     
    System.Windows.Forms.Control 类型定义了大约70个事件,在经编译器转换之后,会生成add 和 remove 访问器方法 以及委托字段。由于大多数程序源只关心少数几个事件,从Control派生类型创建的对象都要浪费大量内存。
     
    这些定义的事件没有被用到,导致了浪费。那么就需要一个方法能将需要的事件定义,不需要的事件不会定义。所以就出现了显式实现事件. 
     
    如何通过显示实现事件来高效率的实现提供了大量事件的类?
     
    为了高效率存储事件委托,公开了事件的每个对象都要维护一个集合(即字典)。 
    集合将事件标识符作为键(key),将委托列表作为值(value)
     
    如何实现这个模式:
     
    1 首先实现一个EventSet 类,它代表一个集合,包含事件以及每个事件的委托列表。 
    2 接着定义一个类来使用 EventSet 类。 在这个类中,一个字段引用了一个EventSet对象,而且这个类的每个事件都是显式实现的,使每个事件的add方法都将指定的 回调委托存储 到 Event对象中,而且每个事件的 remove 方法都删除指定的回调委托。
     
     
     
     
     
     
     
     
     
  • 相关阅读:
    QML学习笔记(三)-引入Font-awesome
    QML学习笔记(一)-防止鼠标穿透事件
    JS小积累(一)— 判断在线离线
    electron入门笔记(三)- 引入bootstrap
    express搭建服务器
    生成SSH密钥添加到GitHub
    python中常见的错误
    PyCharm在同一个包(package)下,如何把一个.py文件导入另外一个.py文件下
    在PyCharm中导入Numpy和Pygame模块 (win8.1)
    Pycharm中安装Pygame并写第一个程序
  • 原文地址:https://www.cnblogs.com/mingjie-c/p/11656668.html
Copyright © 2020-2023  润新知