事件概述

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

基础事件(event)

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

     class Program
{
/// <summary>
/// 定义有参无返回值委托
/// </summary>
/// <param name="i"></param>
public delegate void NoReturnWithParameters();
/// <summary>
/// 定义接受NoReturnWithParameters委托类型的事件
/// </summary>
static event NoReturnWithParameters NoReturnWithParametersEvent;
static void Main(string[] args)
{
//委托方法1
{
Action action = new Action(() =>
{
Console.WriteLine("测试委托方法1成功");
});
NoReturnWithParameters noReturnWithParameters = new NoReturnWithParameters(action);
//事件订阅委托
NoReturnWithParametersEvent += noReturnWithParameters;
//事件取阅委托
NoReturnWithParametersEvent -= noReturnWithParameters;
}
//委托方法2
{
//事件订阅委托
NoReturnWithParametersEvent += new NoReturnWithParameters(() =>
{
Console.WriteLine("测试委托方法2成功");
});
}
//委托方法3
{
//事件订阅委托
NoReturnWithParametersEvent += new NoReturnWithParameters(() => Console.WriteLine("测试委托方法3成功"));
}
//执行事件
NoReturnWithParametersEvent();
Console.ReadKey();
}
/*
* 作者:Jonins
* 出处:http://www.cnblogs.com/jonins/
*/
}

上述代码执行结果:

事件发布&订阅

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

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

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

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

     /// <summary>
/// 发行者
/// </summary>
public class Publisher
{
/// <summary>
/// 委托
/// </summary>
public delegate void Publication(); /// <summary>
/// 事件 这里约束委托类型可以为内置委托Action
/// </summary>
public event Publication AfterPublication;
/// <summary>
/// 来电事件
/// </summary>
public void Call()
{
Console.WriteLine("显示来电");
if (AfterPublication != null)//如果调用列表不为空,触发事件
{
AfterPublication();
}
}
}
/// <summary>
/// 订阅者
/// </summary>
public class Subscriber
{
/// <summary>
/// 订阅者事件处理方法
/// </summary>
public void Connect()
{
Console.WriteLine("通话接通");
}
/// <summary>
/// 订阅者事件处理方法
/// </summary>
public void Unlock()
{
Console.WriteLine("电话解锁");
}
}
/*
* 作者:Jonins
* 出处:http://www.cnblogs.com/jonins/
*/
     class Program
{
static void Main(string[] args)
{
//定义发行者
Publisher publisher = new Publisher();
//定义订阅者
Subscriber subscriber = new Subscriber();
//发行者订阅 当来电需要电话解锁
publisher.AfterPublication += new Publisher.Publication(subscriber.Unlock);
//发行者订阅 当来电则接通电话
publisher.AfterPublication += new Publisher.Publication(subscriber.Connect);
//来电话了
publisher.Call();
Console.ReadKey();
}
}

执行结果:

注意:

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

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

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

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

     class Program
{
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
//------利用多播委托-------
var publication = new Publisher.Publication(subscriber.Unlock);
publication += new Publisher.Publication(subscriber.Connect);
publisher.AfterPublication += publication;
//---------End-----------
publisher.Call();
Console.ReadKey();
}
}

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

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

 private void Form1_Load(object sender, EventArgs e)
{
...
}

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

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

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

基于EventHandler模式的事件

     /// <summary>
/// 事件监听器
/// </summary>
public class Consumer
{
private string _name; public Consumer(string name)
{
_name = name;
}
public void Monitor(object sender, CustomEventArgs e)
{
Console.WriteLine($"Name:{_name}; 信息:{e.Message};到底要不要接呢?");
}
}
/// <summary>
/// 定义保存自定义事件信息的对象
/// </summary>
public class CustomEventArgs : EventArgs//作为事件的参数,必须派生自EventArgs基类
{
public CustomEventArgs(string message)
{
this.Message = message;
}
public string Message { get; set; }
}
/// <summary>
/// 发布者
/// </summary>
public class Publisher
{
public event EventHandler<CustomEventArgs> Publication;//定义事件
public void Call(string w)
{
Console.WriteLine("显示来电." + w);
OnRaiseCustomEvent(new CustomEventArgs(w));
}
//在一个受保护的虚拟方法中包装事件调用。
//允许派生类覆盖事件调用行为
protected virtual void OnRaiseCustomEvent(CustomEventArgs e)
{
//在空校验之后和事件引发之前。制作临时副本,以避免可能发生的事件。
EventHandler<CustomEventArgs> publication = Publication;
//如果没有订阅者,事件将是空的。
if (publication != null)
{
publication(this, e);
}
}
}
/// <summary>
/// 订阅者
/// </summary>
public class Subscriber
{
private string Name;
public Subscriber(string name, Publisher pub)
{
Name = name;
//使用c# 2.0语法订阅事件
pub.Publication += UnlockEvent;
pub.Publication += ConnectEvent;
}
//定义当事件被提起时该采取什么行动。
void ConnectEvent(object sender, CustomEventArgs e)
{
Console.WriteLine("通话接通.{0}.{1}", e.Message, Name);
}
void UnlockEvent(object sender, CustomEventArgs e)
{
Console.WriteLine("电话解锁.{0}.{1}", e.Message, Name);
}
}
/*
* 作者:Jonins
* 出处:http://www.cnblogs.com/jonins/
*/

调用方式:

     class Program
{
static void Main(string[] args)
{
Publisher pub = new Publisher();
//加入一个事件监听
Consumer jack = new Consumer("Jack");
pub.Publication += jack.Monitor;
Subscriber user1 = new Subscriber("中国移动", pub);
pub.Call("号码10086");
Console.WriteLine("--------------------------------------------------");
Publisher pub2 = new Publisher();
Subscriber user2 = new Subscriber("中国联通", pub2);
pub2.Call("号码10010");
Console.ReadKey();
}
}

结果如下:

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

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

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

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

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

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

     Publisher pub = new Publisher();
pub.Publication += Close;
...
//添加一个方法
static void Close(object sender, CustomEventArgs a)
{
// 关闭电话
}

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

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

 EventHandler<CustomEventArgs> publication = Publication;

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

 Publication?.Invoke(this, e);

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

线程安全方式触发事件

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

             //版本1
if (Publication != null)
{
Publication();//触发事件
} //版本2
var temp = Publication;
if (temp != null)
{
temp();//触发事件
} //版本3
var temp = Volatile.Read(ref Publication);
if (temp != null)
{
temp();//触发事件
}

版本1会发生NullReferenceException异常。

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

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

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

事件揭秘

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

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

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

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

 NoReturnWithParameters parameters;
private event NoReturnWithParameters NoReturnWithParametersEvent
{
add { NoReturnWithParametersEvent+=parameters; }
remove { NoReturnWithParametersEvent-=parameters; }
}
/*
* 作者:Jonins
* 出处:http://www.cnblogs.com/jonins/
*/

声明了一个私有的委托变量,开放两个方法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 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

...


事件(event)的更多相关文章

  1. 事件EVENT与waitforsingleobject的使用

    事件event与waitforsingleobject的配合使用,能够解决很多同步问题,也可以在数据达到某个状态时启动另一个线程的执行,如报警. event的几个函数: 1.CreateEvent和O ...

  2. 经典线程同步 事件Event

    阅读本篇之前推荐阅读以下姊妹篇: <秒杀多线程第四篇 一个经典的多线程同步问题> <秒杀多线程第五篇 经典线程同步关键段CS> 上一篇中使用关键段来解决经典的多线程同步互斥问题 ...

  3. C#事件(event)解析

    事件(event),这个词儿对于初学者来说,往往总是显得有些神秘,不易弄懂.而这些东西却往往又是编程中常用且非常重要的东西.大家都知道windows消息处理机制的重要,其实C#事件就是基于window ...

  4. 【温故知新】c#事件event

    从上一篇文章[温故知新]C#委托delegate可知,委托delegate和事件Event非常的相似,区别就是event关键字,给delegate穿上了个“马甲”. 让我们来看官方定义: 类或对象可以 ...

  5. 事件[event]_C#

    事件(event): 1.       事件是类在发生其关注的事情时用来提供通知的方式.例如,封装用户界面控件的类可以定义一个在单击该控件时发生的事件.控件类不关心单击按钮时发生了什么,但它需要告知派 ...

  6. C#中的委托(Delegate)和事件(Event)

    原文地址:C#中的委托(Delegate)和事件(Event) 作者:jiyuan51 把C#中的委托(Delegate)和事件(Event)放到现在讲是有目的的:给下次写的设计模式--观察者(Obs ...

  7. MFC线程(三):线程同步事件(event)与互斥(mutex)

    前面讲了临界区可以用来达到线程同步.而事件(event)与互斥(mutex)也同样可以做到. Win32 API中的线程事件 HANDLE hEvent = NULL; void MainTestFu ...

  8. 重温委托(delegate)和事件(event)

    1.delegate是什么 某种意义上来讲,你可以把delegate理解成C语言中的函数指针,它允许你传递一个类A的方法m给另一个类B的对象,使得类B的对象能够调用这个方法m,说白了就是可以把方法当作 ...

  9. C#总结(二)事件Event 介绍总结

    最近在总结一些基础的东西,主要是学起来很难懂,但是在日常又有可能会经常用到的东西.前面介绍了 C# 的 AutoResetEvent的使用介绍, 这次介绍事件(event). 事件(event),对于 ...

  10. 多线程面试题系列(6):经典线程同步 事件Event

    上一篇中使用关键段来解决经典的多线程同步互斥问题,由于关键段的"线程所有权"特性所以关键段只能用于线程的互斥而不能用于同步.本篇介绍用事件Event来尝试解决这个线程同步问题.首先 ...

随机推荐

  1. LeetCode题解之Second Minimum Node In a Binary Tree

    1.题目描述 2.问题分析 使用set. 3.代码 set<int> s; int findSecondMinimumValue(TreeNode* root) { dfs(root); ...

  2. python第十九天——感冒中

    ConfigParser模块,hashlib模块,hmac模块: 创建配置文件: import configparser config = configparser.ConfigParser()#创建 ...

  3. 移动端上拉加载,下拉刷新效果Demo

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  4. mysqlreport工具

    进行MySQL的配置优化,首先必须找出MySQL的性能瓶颈所在:而SHOW STATUS输出的报告正是用来计算性能瓶颈的参考数据.mysqlreport不像SHOW STATUS那样简单的罗列数据,而 ...

  5. Github API

    Web API web api是网站的一部分,用于与使用非常具体的URL请求特定信息的程序交互,这种请求被称为API调用.请求的数据将以易于处理的格式(如JSON或CSV)返回:依赖于外部数据源的大多 ...

  6. 路由交换02-----ARP协议

    路由交换协议-----ARP ARP协议 ARP(Address Resolution Protocol),是根据IP地址获取MAC地址的一个TCP/IP协议,即将IP地址对应到物理地址,从而实现数据 ...

  7. Alpha冲刺! Day9 - 砍柴

    Alpha冲刺! Day9 - 砍柴 今日已完成 晨瑶:继续补充gitkraken教程. 昭锡:实现主页基本布局. 永盛:进一步了解了框架,为框架生成的模型填充了假数据到数据库. 立强:文章模块基本实 ...

  8. [bug]android monkey命令在Android N和Android O上的一点差异发现

    最近再调试这个统计FPS的代码,发现代码在android N上可以正常运行,但在android O上却运行不了,拼了命的报错,给出的提示就是 ZeroDivisionError: division b ...

  9. Python字符串操作之字符串分割与组合

    12.字符串的分割和组合 12.1 str.split():字符串分割函数 通过指定分隔符对字符串进行切片,并返回分割后的字符串列表. 语法: str.split(s, num)[n] 参数说明: s ...

  10. 【转】MySQL 当记录不存在时insert,当记录存在时update

    MySQL当记录不存在时insert,当记录存在时更新:网上基本有三种解决方法 第一种: 示例一:insert多条记录 假设有一个主键为 client_id 的 clients 表,可以使用下面的语句 ...