首页 > Python基础教程 >
-
C#教程之事件(event)(2)
调用方式:
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 }
结果如下:
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 模式。
线程安全方式触发事件
在上面的例子中,过去常见的触发事件有三种方式:
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。
事件揭秘
我们重新审视基础事件里的一段代码:
1 public delegate void NoReturnWithParameters(); 2 static event NoReturnWithParameters NoReturnWithParametersEvent;
通过反编译我们可以看到:
编译器相当于做了一次如下封装:
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_xxx和remove_xxxC#编译器还自动为方法生成代码调用(System.Delegate的静态方法Combine和Remove)。
4.试图删除从未添加过的方法,Delegate的Remove方法内部不做任何事经,不会抛出异常或任何警告,事件的方法集体保持不变。
5.add和remove方法以线程安全的一种模式更新值(Interlocked Anything模式)。
结语
类或对象可以通过事件向其他类或对象通知发生的相关事情。事件使用的是发布/订阅机制,声明事件的类为发布类,而对这个事件进行处理的类则为订阅类。而订阅类如何知道这个事件发生并处理,这时候需要用到委托。事件的使用离不开委托。但是事件并不是委托的一种(事件是特殊的委托的说法并不正确),委托属于类型(type)它指的是集合(类,接口,结构,枚举,委托),事件是定义在类里的一个成员。
参考文献
CLR via C#(第4版) Jeffrey Richter
C#高级编程(第7版) Christian Nagel (版9、10对事件部分没有多大差异)
果壳中的C# C#5.0权威指南 Joseph Albahari
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/events/index
...