澳门至尊网站-首页

您的位置:澳门至尊网站 > 技术教程 > 学学笔记

学学笔记

2019-10-21 18:32

目录

 

一 常量与字段

事件概述

委托是一种类型可以被实例化,而事件可以看作将多播委托进行封装的一个对象成员(简化委托调用列表增加和删除方法)但并非特殊的委托,保护订阅互不影响。

 

二 事件

基础事件(event)

在.Net中声明事件使用关键词event,使用也非常简单在委托(delegate)前面加上event:

 1     class Program
 2     {
 3         /// <summary>
 4         /// 定义有参无返回值委托
 5         /// </summary>
 6         /// <param name="i"></param>
 7         public delegate void NoReturnWithParameters();
 8         /// <summary>
 9         /// 定义接受NoReturnWithParameters委托类型的事件
10         /// </summary>
11         static event NoReturnWithParameters NoReturnWithParametersEvent;
12         static void Main(string[] args)
13         {
14             //委托方法1
15             {
16                 Action action = new Action(() =>
17                 {
18                     Console.WriteLine("测试委托方法1成功");
19                 });
20                 NoReturnWithParameters noReturnWithParameters = new NoReturnWithParameters(action);
21                 //事件订阅委托
22                 NoReturnWithParametersEvent += noReturnWithParameters;
23                 //事件取阅委托
24                 NoReturnWithParametersEvent -= noReturnWithParameters;
25             }
26             //委托方法2
27             {
28                 //事件订阅委托
29                 NoReturnWithParametersEvent += new NoReturnWithParameters(() =>
30                 {
31                     Console.WriteLine("测试委托方法2成功");
32                 });
33             }
34             //委托方法3
35             {
36                 //事件订阅委托
37                 NoReturnWithParametersEvent += new NoReturnWithParameters(() => Console.WriteLine("测试委托方法3成功"));
38             }
39             //执行事件
40             NoReturnWithParametersEvent();
41             Console.ReadKey();
42         }
43         /*
44          * 作者:Jonins
45          * 出处:http://www.cnblogs.com/jonins/
46          */
47     }

上述代码执行结果:

图片 1

 

一 常量与字段

事件发布&订阅

事件基于委托,为委托提供了一种发布/订阅机制。当使用事件时一般会出现两种角色:发行者订阅者。

发行者(Publisher)也称为发送者(sender):是包含委托字段的类,它决定何时调用委托广播。

订阅者(Subscriber)也称为接受者(recevier):是方法目标的接收者,通过在发行者的委托上调用+=和-=,决定何时开始和结束监听。一个订阅者不知道也不干涉其它的订阅者。

来电->打开手机->接电话,这样一个需求,模拟订阅发布机制:

 1     /// <summary>
 2     /// 发行者
 3     /// </summary>
 4     public class Publisher
 5     {
 6         /// <summary>
 7         /// 委托
 8         /// </summary>
 9         public delegate void Publication();
10 
11         /// <summary>
12         /// 事件  这里约束委托类型可以为内置委托Action
13         /// </summary>
14         public event Publication AfterPublication;
15         /// <summary>
16         /// 来电事件
17         /// </summary>
18         public void Call()
19         {
20             Console.WriteLine("显示来电");
21             if (AfterPublication != null)//如果调用列表不为空,触发事件
22             {
23                 AfterPublication();
24             }
25         }
26     }
27     /// <summary>
28     /// 订阅者
29     /// </summary>
30     public class Subscriber
31     {
32         /// <summary>
33         /// 订阅者事件处理方法
34         /// </summary>
35         public void Connect()
36         {
37             Console.WriteLine("通话接通");
38         }
39         /// <summary>
40         /// 订阅者事件处理方法
41         /// </summary>
42         public void Unlock()
43         {
44             Console.WriteLine("电话解锁");
45         }
46     }
47     /*
48      * 作者:Jonins
49      * 出处:http://www.cnblogs.com/jonins/
50      */

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //定义发行者
 6             Publisher publisher = new Publisher();
 7             //定义订阅者
 8             Subscriber subscriber = new Subscriber();
 9             //发行者订阅 当来电需要电话解锁
10             publisher.AfterPublication += new Publisher.Publication(subscriber.Unlock);
11             //发行者订阅 当来电则接通电话
12             publisher.AfterPublication += new Publisher.Publication(subscriber.Connect);
13             //来电话了
14             publisher.Call();
15             Console.ReadKey();
16         }
17     }

执行结果:

图片 2

注意:

1.事件只可以从声明它们的类中调用, 派生类无法直接调用基类中声明的事件。

1  publisher.AfterPublication();//这行代码在Publisher类外部调用则编译不通过

2.对于事件在声明类外部只能+=,-=不能直接调用,而委托在外部不仅可以使用+=,-=等运算符还可以直接调用。

下面调用方式与上面执行结果一样,利用了委托多播的特性。

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Publisher publisher = new Publisher();
 6             Subscriber subscriber = new Subscriber();
 7             //------利用多播委托-------
 8             var publication = new Publisher.Publication(subscriber.Unlock);
 9             publication += new Publisher.Publication(subscriber.Connect);
10             publisher.AfterPublication += publication;
11             //---------End-----------
12             publisher.Call();
13             Console.ReadKey();
14         }
15     }

 

(一) 常量

 自定义事件(EventArgs&EventHandler&事件监听器)

有过Windwos Form开发经验对下面的代码会熟悉:

1 private void Form1_Load(object sender, EventArgs e)
2 {
3      ...      
4 }

在设计器Form1.Designer.cs中有事件的附加。这种方式属于Visual Studio IDE事件订阅。

1  this.Load += new System.EventHandler(this.Form1_Load);

在 .NET Framework 类库中,事件基于 EventHandler 委托和 EventArgs 基类。

基于EventHandler模式的事件

 1     /// <summary>
 2     /// 事件监听器
 3     /// </summary>
 4     public class Consumer
 5     {
 6         private string _name;
 7 
 8         public Consumer(string name)
 9         {
10             _name = name;
11         }
12         public void Monitor(object sender, CustomEventArgs e)
13         {
14             Console.WriteLine($"Name:{_name}; 信息:{e.Message};到底要不要接呢?");
15         }
16     }
17     /// <summary>
18     /// 定义保存自定义事件信息的对象
19     /// </summary>
20     public class CustomEventArgs : EventArgs//作为事件的参数,必须派生自EventArgs基类
21     {
22         public CustomEventArgs(string message)
23         {
24             this.Message = message;
25         }
26         public string Message { get; set; }
27     }
28     /// <summary>
29     /// 发布者
30     /// </summary>
31     public class Publisher
32     {
33         public event EventHandler<CustomEventArgs> Publication;//定义事件
34         public void Call(string w)
35         {
36             Console.WriteLine("显示来电." + w);
37             OnRaiseCustomEvent(new CustomEventArgs(w));
38         }
39         //在一个受保护的虚拟方法中包装事件调用。
40         //允许派生类覆盖事件调用行为
41         protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
42         {
43             //在空校验之后和事件引发之前。制作临时副本,以避免可能发生的事件。
44             EventHandler<CustomEventArgs> publication = Publication;
45             //如果没有订阅者,事件将是空的。
46             if (publication != null)
47             {
48                 publication(this, e);
49             }
50         }
51     }
52     /// <summary>
53     /// 订阅者
54     /// </summary>
55     public class Subscriber
56     {
57         private string Name;
58         public Subscriber(string name, Publisher pub)
59         {
60             Name = name;
61             //使用c# 2.0语法订阅事件
62             pub.Publication += UnlockEvent;
63             pub.Publication += ConnectEvent;
64         }
65         //定义当事件被提起时该采取什么行动。
66         void ConnectEvent(object sender, CustomEventArgs e)
67         {
68             Console.WriteLine("通话接通.{0}.{1}", e.Message, Name);
69         }
70         void UnlockEvent(object sender, CustomEventArgs e)
71         {
72             Console.WriteLine("电话解锁.{0}.{1}", e.Message, Name);
73         }
74     }
75     /*
76      * 作者:Jonins
77      * 出处:http://www.cnblogs.com/jonins/
78      */

调用方式:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             Publisher pub = new Publisher();
 6             //加入一个事件监听
 7             Consumer jack = new Consumer("Jack");
 8             pub.Publication += jack.Monitor;
 9             Subscriber user1 = new Subscriber("中国移动", pub);
10             pub.Call("号码10086");
11             Console.WriteLine("--------------------------------------------------");
12             Publisher pub2 = new Publisher();
13             Subscriber user2 = new Subscriber("中国联通", pub2);
14             pub2.Call("号码10010");
15             Console.ReadKey();
16         }
17     }

结果如下:

图片 3

1.EventHandler<T>在.NET Framework 2.0中引入,定义了一个处理程序,它返回void,接受两个参数。

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

第一个参数(sender)是一个对象,包含事件的发送者。
第二个参数(e)提供了事件的相关信息,参数随不同的事件类型而改变(继承EventArgs)。
.NET1.0为所有不同数据类型的事件定义了几百个委托,有了泛型委托EventHandler<T>后,不再需要委托了。

2.EventArgs,标识表示包含事件数据的类的基类,并提供用于不包含事件数据的事件的值。

1 [System.Runtime.InteropServices.ComVisible(true)]
2 public class EventArgs

3.同时可以根据编程方式订阅事件

1     Publisher pub = new Publisher();
2     pub.Publication += Close;
3     ...
4     //添加一个方法
5     static void Close(object sender, CustomEventArgs a)
6     {
7             // 关闭电话
8     }

4.Consumer类为事件监听器当触发事件时可获取当前发布者对应自定义信息对象,可以根据需要做逻辑编码,再执行事件所订阅的相关处理。增加事件订阅/发布机制的健壮性。

5.以线程安全的方式触发事件    

1 EventHandler<CustomEventArgs> publication = Publication;

触发事件是只包含一行代码的程序。这是C#6.0的功能。在之前版本,触发事件之前要做为空判断。同时在进行null检测和触发之间,可能另一个线程把事件设置为null。所以需要一个局部变量。在C#6.0中,所有触发都可以使用null传播运算符和一个代码行取代。

1 Publication?.Invoke(this, e);

注意:尽管定义的类中的事件可基于任何有效委托类型,甚至是返回值的委托,但一般还是建议使用 EventHandler 使事件基于 .NET Framework 模式。

 

  常量总是被视为静态成员,而不是实例成员。定义常量将导致创建元数据。代码引用一个常量时,编译器会在定义常量的程序集的元数据中查找该符号,提取常量的值,并将值嵌入IL中。由于常量的值直接嵌入IL,所以在运行时不需要为常量分配任何内存。此外,不能获取常量的地址,也不能以传递引用的方式传递常量。这些限制意味着,没有很好的跨程序集版本控制特性。因此,只有在确定一个符号的值从不变化时,才应该使用。如果希望在运行时从一个程序集中提取一个程序集中的值,那么不应该使用常量,而应该使用 readonly 字段。

线程安全方式触发事件

在上面的例子中,过去常见的触发事件有三种方式:

 1             //版本1
 2             if (Publication != null)
 3             {
 4                 Publication();//触发事件
 5             }
 6 
 7             //版本2
 8             var temp = Publication;
 9             if (temp != null)
10             {
11                 temp();//触发事件
12             }
13 
14             //版本3
15             var temp = Volatile.Read(ref Publication);
16             if (temp != null)
17             {
18                 temp();//触发事件
19             }

版本1会发生NullReferenceException异常。

版本2的解决思路是,将引用赋值到临时变量temp中,后者引用赋值发生时的委托链。所以temp复制后即使另一个线程更改了AfterPublication对象也没有关系。委托是不可变得,所以理论上行得通。但是编译器可能通过完全移除变量temp的方式对上述代码进行优化所以仍可能抛出NullReferenceException.

版本3Volatile.Read()的调用,强迫Publication在这个调用发生时读取,引用真的必须赋值到temp中,编译器优化代码。然后temp只有再部位null时才被调用。

版本3最完美技术正确,版本2也是可以使用的,因为JIT编译机制上知道不该优化掉变量temp,所以在局部变量中缓存一个引用,可确保堆应用只被访问一次。但将来是否改变不好说,所以建议采用版本3。

 

 

图片 4

事件揭秘

我们重新审视基础事件里的一段代码:

1     public delegate void NoReturnWithParameters();
2     static event NoReturnWithParameters NoReturnWithParametersEvent;

通过反编译我们可以看到:

图片 5

编译器相当于做了一次如下封装:

 1 NoReturnWithParameters parameters;
 2 private event NoReturnWithParameters NoReturnWithParametersEvent
 3 {
 4      add {  NoReturnWithParametersEvent+=parameters; }
 5      remove {  NoReturnWithParametersEvent-=parameters; }
 6 }
 7 /*
 8  * 作者:Jonins
 9  * 出处:http://www.cnblogs.com/jonins/
10  */

声明了一个私有的委托变量,开放两个方法add和remove作为事件访问器用于(+=、-=),NoReturnWithParametersEvent被编译为Private从而实现封装外部无法触发事件。

1.委托类型字段是对委托列表头部的引用,事件发生时会通知这个列表中的委托。字段初始化为null,表明无侦听者等级对该事件的关注。

2.即使原始代码将事件定义为Public,委托字段也始终是Private.目的是防止外部的代码不正确的操作它。

3.方法add_xxxremove**_xxxC#编译器还自动为方法生成代码调用(System.Delegate的静态方法CombineRemove**)。

4.试图删除从未添加过的方法,Delegate的Remove方法内部不做任何事经,不会抛出异常或任何警告,事件的方法集体保持不变。

5.**addremove方法以线程安全**的一种模式更新值(Interlocked Anything模式)。

 

(二) 字段

结语

类或对象可以通过事件向其他类或对象通知发生的相关事情。事件使用的是发布/订阅机制,声明事件的类为发布类,而对这个事件进行处理的类则为订阅类。而订阅类如何知道这个事件发生并处理,这时候需要用到委托。事件的使用离不开委托。但是事件并不是委托的一种(事件是特殊的委托的说法并不正确),委托属于类型(type)它指的是集合(类,接口,结构,枚举,委托),事件是定义在类里的一个成员。

 

  CLR支持类型字段和实例字段。对于类型字段,用于容纳字段数据的动态内存是在类型对象中分配的,而类型对象是在类型加载到一个AppDomain时创建的;对于实例字段,用于容纳字段数据的动态内存则是在构造类型的一个实例时分配的。字段解决了版本控制问题,其值存储在内存中,只有在运行时才能获取。

参考文献

 

CLR via C#(第4版) Jeffrey Richter

C#高级编程(第7版) Christian Nagel  (版9、10对事件部分没有多大差异)

果壳中的C# C#5.0权威指南 Joseph Albahari

...


 

  如果字段是引用类型,且被标记为readonly,那么不可改变的是引用,而非字段引用的对象。

(三) 常量与只读字段的区别

  readonly和const本质上都是常量,readonly是运行时常量而const是编译期常量。两种常量具有以下区别:

  • 编译期常量的值在编译时获得,而运行时常量的值在运行时获得。
  • 两者访问方式不同。编译期常量的值是在目标代码中进行替换的,而运行时常量将在运行时求值,引用运行时常量生成的IL将引用到readonly的变量,而不是变量的值。因此,编译期常量的性能更好,而运行时常量更为灵活。
  • 编译期常量仅支持整型、浮点型、枚举和字符串,其它值类型如DateTime是无法初始化编译期常量的。然而,运行时常量则支持任何类型。
  • 编译期常量是静态常量,而运行时常量是实例常量,可以为类型的每个实例存放不同的值。

  综上所述,除非需要在编译期间得到确切的数值以外,其它情况,都应该尽量使用运行时常量。

(四) 常量与字段的设计

  • 不要提供公有的或受保护的实例字段,应该始终把字段定义为private。
  • 要用常量字段来表示永远不会改变的常量。
  • 要用公有的静态只读字段定义预定义的对象实例。
  • 不要把可变类型的实例赋值给只读字段。

二 事件

  如果类型定义了事件,那么类型(或类型实例)就可以通知其它对象发送了特定的事情。如果定义了事件成员,那样类型要提供以下能力:

  • 方法可以登记对事件的关注。
  • 方法可以注销对事件的关注。
  • 事件发送时,关注该事件的方法会收到通知。

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

(一) 如何使用事件

  下例显示了如何使用事件:

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

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            CostomEventPublisher cep = new CostomEventPublisher();
            CostomEventListener cel = new CostomEventListener(cep);
            cep.FireEvent("Hello");
            cep.FireEvent("Word");
            Console.ReadLine();
        }
    }

    //自定义事件参数
    internal sealed class CostomEventArgs : EventArgs
    {
        private readonly string message;

        public string Message
        {
            get { return message; }
        }

        public CostomEventArgs(string message)
        {
            this.message = message;
        }
    }

    //定义事件发布者
    internal class CostomEventPublisher
    {
        //定义事件
        public event EventHandler<CostomEventArgs> CostomEvent;

        //引发事件
        protected virtual void OnCostomEvent(CostomEventArgs e)
        {
            e.Raise(this, ref CostomEvent, false);
        }

        //构造参数实例,并引发事件
        public void FireEvent(string message)
        {
            CostomEventArgs e = new CostomEventArgs(message);
            OnCostomEvent(e);
        }
    }

    //扩展方法封装线程安全逻辑
    public static class EventArgExtensions
    {
        public static void Raise<T>(this T e, Object sender, ref EventHandler<T> eventDelegate, bool ifIgnoreException) where T : EventArgs
        {
            EventHandler<T> temp = Interlocked.CompareExchange(ref eventDelegate, null, null);
            if (temp != null)
            {
                if (!ifIgnoreException)
                {
                    try
                    {
                        temp(sender, e);
                    }
                    catch
                    {
                        //TODO:处理异常
                    }
                }
                else
                {
                    Delegate[] delegates = temp.GetInvocationList();
                    foreach (Delegate del in delegates)
                    {
                        try
                        {
                            temp(sender, e);
                        }
                        catch
                        { }
                    }
                }
            }
        }
    }

    //定义监听者
    internal sealed class CostomEventListener
    {
        //添加事件监听
        public CostomEventListener(CostomEventPublisher costomEventManager)
        {
            costomEventManager.CostomEvent += showMessage;
        }

        //响应方法
        private void showMessage(object sender, CostomEventArgs e)
        {
            Console.WriteLine(e.Message);
        }

        //移除事件监听
        public void Unregister(CostomEventPublisher costomEventManager)
        {
            costomEventManager.CostomEvent -= showMessage;
        }
    }
}

第一步 自定义事件参数

  应该在EventArgs派生类中为事件处理程序提供参数,并将这些参数作为类成员。委托类中遍历他的订阅者列表,将参数对象在订阅者中依次传递。但无法防止某个订阅者修改参数值,进而影响其后所有的处理事件的订阅者。通常情况下,当这些成员在订阅者中传递时,应防止订阅者对其进行修改,可将参数的访问权限设置为只读,或公开这些参数为公共成员,并应用readonly访问修饰符,在这两种情况下,都应该在构造器中初始化这些参数。

第二步 定义委托签名

  虽然委托声明可以定义任何方法签名,但在实践中事件委托应该符合一些特定的指导方针,主要包括:

  • 首先,目标方法的返回类型应为void。使用void的原因是,向事件发布者返回一个值毫无意义,发布者不知道事件订阅者为什么要订阅,此外,委托类向发布者隐藏了实际发布操作。该委托对其内部接收器列表进行遍历(订阅对象),调用每个相应的方法,因此返回的值不会传播到发布者的代码。使用void返回类型还建议我们避免使用包含ref或out参数修饰符的输出参数,因为各个订阅者的输出参数不会传播给发布者。
  • 其次,一些订阅者可能想要从多个事件发布源接收相同的事件。为了让订阅者区分出不同的发布者触发的事件,签名应包含发布者的标识。在不依赖泛型的情况下,最简单的方式就是添加一个object类型的参数,称为发送者(sender)参数。之所以要求sender参数是object类型,主要是由于继承。另一个原因是灵活性。它允许委托由多个类型使用,只有这些类型提供了一个会传递相应的事件参数的事件。
  • 最后,定义实际事件参数将订阅者与发布者耦合起来,因为订阅者需要一组特定的参数。.NET提供了EventArgs类,作为规范是事件参数容器。

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

  类应定义一个受保护的虚方法。要引发事件时,当前类及其派生类中的代码会调用该方法。

第四步 防御式发布事件

  在.NET中,如果委托在其内部列表中没有目标,它的值将设置为null。C#发布者在尝试调用委托之前,应该检查该委托是否为null,以判断是否有订阅者订阅事件。

  另一个需要注意的问题是异常。所有未处理的订阅者引发的异常都会传播给发布者,导致发布者崩溃。所以,使用时最好在try/catch块内部发布事件。

  还需要注意的是线程安全,上例给出了一种线程安全的事件引发代码。考虑线程竟态条件应该意识到的一个重点是,一个方法可能在事件的委托列表中移除之后得到调用。

(二) 管理大量事件

  处理大量事件的问题在于,为每个事件都分配一个类成员是不现实的。为解决此问题,.NET提供了EventHandlerList类。EventHandlerList是存储键/值对的线性列表。键是标识事件的对象,值是Delegate的实例。因为索引是一个对象,所以它可以是整数、字符串、特定的按钮实例等等。使用AddHandler和RemoveHandler方法可以分别添加和删除各个事件处理方法。还可以使用AddHandlers()方法添加现有EventHandlerList的内容。要触发事件,用带键值对象的索引器来访问事件列表,得到一个Delegate对象。将该为他转换为实际事件委托,然后触发事件。实例代码如下:

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

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            CostomEventPublisher cep = new CostomEventPublisher();
            CostomEventListener cel = new CostomEventListener(cep);
            cep.FireClick("Hello");
            cep.FireDoubleClick("Word");
            Console.ReadLine();
        }
    }

    //自定义事件参数
    internal sealed class CostomEventArgs : EventArgs
    {
        private readonly string message;

        public string Message
        {
            get { return message; }
        }

        public CostomEventArgs(string message)
        {
            this.message = message;
        }
    }

    //定义事件发布者
    internal class CostomEventPublisher
    {
        EventHandlerList eventList;
        static object eventClickKey = new object();//使用预分配静态变量作为键,以减少托管堆压力。
        static object eventDoubleClickKey = new object();

        public CostomEventPublisher()
        {
            eventList = new EventHandlerList();
        }

        //定义事件
        public event EventHandler<CostomEventArgs> Click
        {
            add
            {
                eventList.AddHandler(eventClickKey, value);
            }
            remove
            {
                eventList.RemoveHandler(eventClickKey, value);
            }
        }
        public event EventHandler<CostomEventArgs> DoubleClick
        {
            add
            {
                eventList.AddHandler(eventDoubleClickKey, value);
            }
            remove
            {
                eventList.RemoveHandler(eventDoubleClickKey, value);
            }
        }

        //引发事件
        protected virtual void OnEvent<T>(T e, EventHandler<T> eventDelegate) where T : EventArgs
        {
            e.Raise(this, ref eventDelegate, false);
        }

        //构造参数实例,并引发事件
        public void FireClick(string message)
        {
            CostomEventArgs e = new CostomEventArgs("Click:" + message);
            OnEvent(e, eventList[eventClickKey] as EventHandler<CostomEventArgs>);
        }

        public void FireDoubleClick(string message)
        {
            CostomEventArgs e = new CostomEventArgs("Double:" + message);
            OnEvent(e, eventList[eventDoubleClickKey] as EventHandler<CostomEventArgs>);
        }
    }

    //扩展方法封装线程安全逻辑
    public static class EventArgExtensions
    {
        public static void Raise<T>(this T e, Object sender, ref EventHandler<T> eventDelegate, bool ifIgnoreException) where T : EventArgs
        {
            EventHandler<T> temp = Interlocked.CompareExchange(ref eventDelegate, null, null);
            if (temp != null)
            {
                if (!ifIgnoreException)
                {
                    try
                    {
                        temp(sender, e);
                    }
                    catch
                    {
                        //TODO:处理异常
                    }
                }
                else
                {
                    Delegate[] delegates = temp.GetInvocationList();
                    foreach (Delegate del in delegates)
                    {
                        try
                        {
                            temp(sender, e);
                        }
                        catch
                        { }
                    }
                }
            }
        }
    }

    //定义监听者
    internal sealed class CostomEventListener
    {
        //添加事件监听
        public CostomEventListener(CostomEventPublisher costomEventManager)
        {
            costomEventManager.Click += showMessage;
            costomEventManager.DoubleClick += showMessage;
        }

        //响应方法
        private void showMessage(object sender, CostomEventArgs e)
        {
            Console.WriteLine(e.Message);
        }

        //移除事件监听
        public void Unregister(CostomEventPublisher costomEventManager)
        {
            costomEventManager.Click -= showMessage;
            costomEventManager.DoubleClick -= showMessage;
        }
    }
}

(三)  封装事件访问器及成员

  通过隐藏实际事件成员,事件访问器提供了一定程度的封装。这还不够,通过编写订阅者接口进一步封装。实例代码如下:

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

namespace Test
{
    class Program
    {
        static void Main(string[] args)
        {
            CostomEventPublisher cep = new CostomEventPublisher();
            CostomEventListener cel = new CostomEventListener();
            cep.Subscribe(cel, EventType.OnAllEvents);
            cep.FireEvent(EventType.OnClick, "Click:Hello");
            cep.FireEvent(EventType.OnDoubleClick, "Double:Word");
            Console.ReadLine();
        }
    }

    //自定义事件参数
    public sealed class CostomEventArgs : EventArgs
    {
        private readonly string message;

        public string Message
        {
            get { return message; }
        }

        public CostomEventArgs(string message)
        {
            this.message = message;
        }
    }

    //定义监听者接口
    public interface ICostomEventListener
    {
        void OnClick(object sender, CostomEventArgs eventArgs);
        void OnDoubleClick(object sender, CostomEventArgs eventArgs);
    }

    //定义事件类型枚举
    [Flags]
    public enum EventType
    {
        OnClick = 0x01,
        OnDoubleClick = 0x02,
        OnAllEvents = OnClick | OnDoubleClick
    }

    //定义事件发布者
    internal class CostomEventPublisher
    {
        EventHandlerList eventList;
        static object eventClickKey = new object();
        static object eventDoubleClickKey = new object();

        public CostomEventPublisher()
        {
            eventList = new EventHandlerList();
        }

        //定义事件
        public event EventHandler<CostomEventArgs> Click
        {
            add
            {
                eventList.AddHandler(eventClickKey, value);
            }
            remove
            {
                eventList.RemoveHandler(eventClickKey, value);
            }
        }
        public event EventHandler<CostomEventArgs> DoubleClick
        {
            add
            {
                eventList.AddHandler(eventDoubleClickKey, value);
            }
            remove
            {
                eventList.RemoveHandler(eventDoubleClickKey, value);
            }
        }

        //引发事件
        protected virtual void OnEvent<T>(T e, EventHandler<T> eventDelegate) where T : EventArgs
        {
            e.Raise(this, ref eventDelegate, false);
        }

        //添加事件监听
        public void Subscribe(ICostomEventListener listener, EventType type)
        {
            if ((type & EventType.OnClick) == EventType.OnClick)//判断是否包含某个枚举值
            {
                this.Click += listener.OnClick;
            }
            if ((type & EventType.OnDoubleClick) == EventType.OnDoubleClick)
            {
                this.DoubleClick += listener.OnDoubleClick;
            }
        }

        //移除事件监听
        public void Unsubscribe(ICostomEventListener listener, EventType type)
        {
            if ((type & EventType.OnClick) == EventType.OnClick)
            {
                this.Click -= listener.OnClick;
            }
            if ((type & EventType.OnDoubleClick) == EventType.OnDoubleClick)
            {
                this.DoubleClick -= listener.OnDoubleClick;
            }
        }

        //构造参数实例,并引发事件
        public void FireEvent(EventType type, string message)
        {
            CostomEventArgs e = new CostomEventArgs(message);
            if ((type & EventType.OnClick) == EventType.OnClick)
            {
                OnEvent(e, eventList[eventClickKey] as EventHandler<CostomEventArgs>);
            }
            if ((type & EventType.OnDoubleClick) == EventType.OnDoubleClick)
            {
                OnEvent(e, eventList[eventDoubleClickKey] as EventHandler<CostomEventArgs>);
            }
        }
    }

    //扩展方法封装线程安全逻辑
    public static class EventArgExtensions
    {
        public static void Raise<T>(this T e, Object sender, ref EventHandler<T> eventDelegate, bool ifIgnoreException) where T : EventArgs
        {
            EventHandler<T> temp = Interlocked.CompareExchange(ref eventDelegate, null, null);
            if (temp != null)
            {
                if (!ifIgnoreException)
                {
                    try
                    {
                        temp(sender, e);
                    }
                    catch
                    {
                        //TODO:处理异常
                    }
                }
                else
                {
                    Delegate[] delegates = temp.GetInvocationList();
                    foreach (Delegate del in delegates)
                    {
                        try
                        {
                            temp(sender, e);
                        }
                        catch
                        { }
                    }
                }
            }
        }
    }

    //定义监听者
    internal sealed class CostomEventListener : ICostomEventListener
    {
        //响应方法
        private void showMessage(object sender, CostomEventArgs e)
        {
            Console.WriteLine(e.Message);
        }

        public void OnClick(object sender, CostomEventArgs eventArgs)
        {
            showMessage(sender, eventArgs);
        }
        public void OnDoubleClick(object sender, CostomEventArgs eventArgs)
        {
            showMessage(sender, eventArgs);
        }
    }
}

  上面代码显示了这种方法的优点,仅通过一次调用就可以通知整个接口,并显示了对事件类成员的完全封装。

(四) 事件的本质

  事件是一个类为委托的字段再加上两个对字段进行操作的方法。事件是委托列表头部的引用,是为了简化和改善使用委托来作为应用程序的回调机制时的编码。.NET事件支持依赖于委托,以下代码显示了在未使用事件时的编码:

    class Program
    {
        static void Main(string[] args)
        {
            Bell bell = new Bell();
            bell.RingList = new Bell.Ring(CallWhenRingA);
            bell.Rock(1);
            //赋值新对象
            bell.RingList = new Bell.Ring(CallWhenRingB);
            bell.Rock(2);
            //直接调用委托
            bell.RingList.Invoke(3);

            Console.ReadLine();
        }

        static void CallWhenRingA(int times)
        {
            Console.WriteLine("A"+times);
        }

        static void CallWhenRingB(int times)
        {
            Console.WriteLine("B" + times);
        }
    }

    public sealed class Bell
    {
        public delegate void Ring(int times);

        public Ring RingList;

        public void Rock(int times)
        {
            if (RingList != null)
            {
                RingList(times);
            }
        }
    }

  以上代码存在下列问题——发布类需要将委托成员公开为公共成员变量,这样所有参与方都可以向该委托列表添加订阅者,公共的委托成员打破了封装,导致代码应用程序安全风险。因此为了解决该问题,并简化编码微软给出了event来细化作为事件订阅和通知使用的委托类型。将委托成员变量定义为事件后,即使该成员为公共成员,也仅有发布类(不包括子类)可以出发此事件(虽然任何人都可以向该委托列表添加目标方法)。由发布类的开发者来决定是否提供一个公共方法来触发该事件。使用事件代替原始委托还会降低发布者与订阅者间的松耦合,因为发布者触发事件的业务逻辑对订阅者是隐蔽的。

1 事件访问器

  事件访问器类似于属性,在易于使用的同时隐藏了实际类成员。

  CostomEventReleaser中使用以下代码定义事件:

    public event EventHandler<CostomEventArgs> CostomEvent;

  这段代码是定义事件的一种缩写形式,它会隐式定义添加和删除处理程序的方法并声明委托的一个变量。当编译器编译这段代码时,会把它转换为以下3个构造:

图片 6

2 隐式实现事件

  我们在上例中看到的事件的完整定义会转换为以下3个构造:

   style="font-size: 12px;">//1 一个被初始化为null的私有委托字段

style="font-size: 12px;">  private EventHandler<CostomEventArgs> CostomEvent = null;

 

  //2 一个公共add_xxx方法(xxx代表事件名),用于添加事件订阅

  public void add_CostomEvent(EventHandler<CostomEventArgs> value)

  {

style="font-size: 12px;">    EventHandler<CostomEventArgs> prevHandler;

style="font-size: 12px;">    EventHandler<CostomEventArgs> costomEvent =this.CostomEvent ;

    do

    {

      prevHandler=costomEvent ;

style="font-size: 12px;">      EventHandler<CostomEventArgs> newHandler=(EventHandler<CostomEventArgs>)Delegate.Combine(prevHandler,value);

      costomEvent =Interlocked.CompareExchange<EventHandler<CostomEventArgs>>(ref this.CostomEvent,newHandler,prevHandler);//通过循环和对CompareExchange的调用,可以以一种线程安全的方式向事件添加一个委托。

    }

    while(costomEvent != prevHandler);

  }

  

  //3 一个公共remove_xxx方法,用于取消事件订阅

  public void remove_CostomEvent(EventHandler<CostomEventArgs> value)

  {

style="font-size: 12px;">    EventHandler<CostomEventArgs> prevHandler;

style="font-size: 12px;">    EventHandler<CostomEventArgs> costomEvent =this.CostomEvent ;

    do

    {

      prevHandler=costomEvent ;

style="font-size: 12px;">      EventHandler<CostomEventArgs> newHandler=(EventHandler<CostomEventArgs>)Delegate.Remove(prevHandler,value);

      costomEvent =Interlocked.CompareExchange<EventHandler<CostomEventArgs>>(ref this.CostomEvent,newHandler,prevHandler);//通过循环和对CompareExchange的调用,可以以一种线程安全的方式向事件移除一个委托。

    }

    while(costomEvent != prevHandler);

  }

  除了生成上述3个构造,编译器还会在托管程序集的元数据中生成一个事件定义纪录项。这个记录项包含一些标志和基础委托类型,还引用了add和remove访问器方法。这些信息的作用是建立“事件”的抽象概念和它的访问器方法之间的联系。编译器和其它工具可以利用这些元数据信息,并可通过System.Reflection.EventInfo类获取这些信息。但是,CLR本身并不使用这些信息,它在运行时只需要访问器方法。

(五) 事件与自定义处理函数的设计

1 事件的设计

  • 要用System.EventHandler<T>来定义事件处理函数,而不是手工创建新的委托来定义事件处理函数。
  • 考虑用EventArgs的子类来做事件的参数,除非百分之百确信该事件不需要给事件处理方法传递任何数据,在这种情况下可以直接使用EventArgs。
  • 要用受保护的虚方法来触发事件,一般方法以名字“On”开头,随后是事件名字。该规则只适用于非密封类中的非静态事件,不适用于结构、密封类及静态事件。派生类在覆盖虚方法时可以不调用基类的实现,要准备好应对这种情况,不要在该方法中做任何对基类来说不可或缺的处理。
  • 如果类中有一个事件,那么在调用委托之前需要加一个非空测试,其代码形如:“ClickHandler handler=Click;if(handler !=null) handler(this,e);”。
  • 编译器生成的用来添加和删除事件处理方法的代码在多线程中不安全,所以如果需要支持让多线程同时添加或去除事件处理方法,那么需要自己编写代码来添加和去除事件处理方法,并在内部进行锁操作。
  • 要让触发事件的受保护方法带一个参数,该参数的类型为事件参数类,该参数的名字应该为“e”。
  • 不要在触发非静态事件时把null作为sender参数传入。
  • 要在触发静态事件时把null作为sender参数传入。
  • 不要在触发事件时把null作为数据参数传入,如果不行传任何数据应该使用EventArgs.Empty。
  • 考虑使用CancelEventArgs或它的子类作为参数,来触发能够被最终用户取消的事件,这只适应于前置事件。代码形如:“void ColeingHandler(object sender,CancelEventArgse){ e.Cancel=true;}”。

2 自定义处理函数的设计

  • 把事件处理函数的返回值类型定义为void。
  • 要用object作为事件处理函数的第一个参数的类型,并将其命名为sender。
  • 要用EventArgs或其子类作为事件处理函数的第二个参数的类型,并将其命名为e。
  • 不要在事件处理函数中使用两个以上的参数。

(六).NET松耦合事件

  .NET事件简化了事件管理,它使我们不用去写管理订阅者列表的繁琐的代码。但是基于委托的事件还是存在以下缺陷:

  • 对于每个要从其中接收事件的发布者对象,订阅者都必须重复添加订阅的代码,没有一种方法可以订阅某个类型的事件并使该事件传递给订阅者,而不管发布者是谁。
  • 订阅者无法筛选已触发是事件(例如,提示“仅在满足某种条件是才通知我该事件”)。
  • 订阅者必须有某种办法获得发布者对象才能对其进行订阅,这样就造成了订阅者和发布者之间以及各个订阅者之间的耦合。
  • 发布者和订阅者具有耦合的生命周期,两者必须同时运行。订阅者无法通知.NET“如果任何对象触发此事件,请创建一个我的实例,并由我来处理”。
  • 没有捷径执行取消订阅操作,发布者对象在脱机是计算机上触发事件,一旦计算机处于联机状态,该事件便会传给订阅者。反之,订阅者运行在脱机的计算机上,一旦处于联机状态,接收到在断开连接时触发是事件也是可能的。
  • 订阅的建立和取消必须通过编程方式完成。

  .Net和其它第三方框架支持松耦合事件,在.NET中定义松耦合事件可以参考MSDN中关于System.EnterpriseServices.ServicedComponent的相关内容。

本文由澳门至尊网站发布于技术教程,转载请注明出处:学学笔记

关键词:

  • 上一篇:没有了
  • 下一篇:没有了