事件概述
委托是一种类型可以被实例化,而事件可以看作将多播委托进行封装的一个对象成员(简化委托调用列表增加和删除方法)但并非特殊的委托,保护订阅互不影响。
基础事件(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 }
上述代码执行结果:
事件发布&订阅
事件基于委托,为委托提供了一种发布/订阅机制。当使用事件时一般会出现两种角色:发行者和订阅者。
发行者(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 }
执行结果:
注意:
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 }
结果如下:
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
...