C#学习笔记——常量、字段以及事件
(一) 常量
常量总是被视为静态成员,而不是实例成员。定义常量将导致创建元数据。代码引用一个常量时,编译器会在定义常量的程序集的元数据中查找该符号,提取常量的值,并将值嵌入IL中。由于常量的值直接嵌入IL,所以在运行时不需要为常量分配任何内存。此外,不能获取常量的地址,也不能以传递引用的方式传递常量。这些限制意味着,没有很好的跨程序集版本控制特性。因此,只有在确定一个符号的值从不变化时,才应该使用。如果希望在运行时从一个程序集中提取一个程序集中的值,那么不应该使用常量,而应该使用 readonly 字段。
(二) 字段
CLR支持类型字段和实例字段。对于类型字段,用于容纳字段数据的动态内存是在类型对象中分配的,而类型对象是在类型加载到一个AppDomain时创建的;对于实例字段,用于容纳字段数据的动态内存则是在构造类型的一个实例时分配的。字段解决了版本控制问题,其值存储在内存中,只有在运行时才能获取。
如果字段是引用类型,且被标记为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个构造:
2 隐式实现事件
我们在上例中看到的事件的完整定义会转换为以下3个构造:
//1 一个被初始化为null的私有委托字段
private EventHandler<CostomEventArgs> CostomEvent = null;
//2 一个公共add_xxx方法(xxx代表事件名),用于添加事件订阅
public void add_CostomEvent(EventHandler<CostomEventArgs> value)
{
EventHandler<CostomEventArgs> prevHandler;
EventHandler<CostomEventArgs> costomEvent =this.CostomEvent ;
do
{
prevHandler=costomEvent ;
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)
{
EventHandler<CostomEventArgs> prevHandler;
EventHandler<CostomEventArgs> costomEvent =this.CostomEvent ;
do
{
prevHandler=costomEvent ;
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的相关内容。