C#学习笔记——常量、字段以及事件


目录

 


常量与字段

事件概述

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

 


事件

基础事件(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     }

上述代码执行结果:

永利皇宫463手机版 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     }

执行结果:

永利皇宫463手机版 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     }

结果如下:

永利皇宫463手机版 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。

 

 

永利皇宫463手机版 4

事件揭秘

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

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

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

永利皇宫463手机版 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。
  • 要用常量字段来表示永远不会改变的常量。
  • 要用公有的静态只读字段定义预定义的对象实例。
  • 永利皇宫463手机版,不要把可变类型的实例赋值给只读字段。


事件

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

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

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

(一) 如何使用事件

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

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,以判断是否有订阅者订阅事件。

发表评论

电子邮件地址不会被公开。 必填项已用*标注