由浅入深理解C#中的事件
目录
本文较长,给大家提供了目录,可以直接看自己感兴趣的部分。
前言
有关事件的概念
示例
简单示例
标准 .NET 事件模式
使用泛型版本的标准 .NET 事件模式
补充
总结
参考
前言
前面介绍了C#中的委托,事件的很多部分都与委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托,事件包含了一个私有的委托,如下图所示:

有关事件的私有委托需要了解的重要事项如下:
1、事件提供了对它的私有控制委托的结构化访问。我们无法直接访问该委托。
2、事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。
3、事件被触发时,它调用委托来依次调用调用列表中的方法。
有关事件的概念
发布者(Publisher):发布某个事件的类或结构,其他类可以在该事件发生时得到通知。
订阅者(Subscriber):注册并在事件发生时得到通知的类或结构。
事件处理程序(event handler):由订阅者注册到事件的方法,在发布者触发事件时执行。
触发(raise)事件:调用(invoke)或触发(fire)事件的术语。当事件触发时,所有注册到它的方法都会被依次调用。
示例
简单示例
现在我们先来看一下最最原始的事件示例。其结构如下所示:

委托类型声明:事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。
事件处理程序声明:订阅者类中会在事件触发时执行的方法声明。它们不一定有显示命名的方法,还可以是匿名方法或Lambda表达式。
事件声明:发布者类必须声明一个订阅者类可以注册的事件成员。当声明的事件为public时,称为发布了事件。
事件注册:订阅者必须订阅事件才能在它被触发时得到通知。
触发事件的代码:发布者类中”触发“事件并导致调用注册的所有事件处理程序的代码。
现在我们可以照着这个思路去写示例代码。
首先声明一个自定义的委托类型:
public delegate void MyDelegate();
该委托类型没有参数也没有返回值。
然后再写一个发布者类:
public class Publisher
{
public event MyDelegate MyEvent;
public void DoCount()
{
for(int i = 0; i < 10; i++)
{
Task.Delay(3000).Wait();
//确认有方法可以执行
if(MyEvent != null)
{
//触发事件
MyEvent();
}
}
}
}
事件声明:
public event MyDelegate MyEvent;
事件声明在一个类中,它需要委托类型的名称,任何注册到事件的处理程序都必须与委托类型的签名和返回类型匹配。它声明为public,这样其他类和结构可以在它上面注册事件处理程序。不能使用对象创建表达式(new表达式)来创建它的对象。
一个常见的误解就是把事件认为是类型,事件其实不是类型,它和方法、属性一样是类或结构的成员。
由于事件是成员,所以我们不能在一段可执行的代码中声明事件,它必须声明在类或结构中,和其他成员一样。
事件成员被隐式自动初始化为null。
事件声明的图解如下所示:

触发事件:
//确认有方法可以执行
if(MyEvent != null)
{
//触发事件
MyEvent();
}
也可以这样写:
//确认有方法可以执行
if(MyEvent != null)
{
//触发事件
MyEvent().Invoke();
}
这两者是等效的,MyEvent();直接调用事件的委托,MyEvent().Invoke()使用显式调用委托的 Invoke 方法。
现在再看看订阅者类:
public class Subscriber
{
public void EventHandler()
{
Console.WriteLine($"{DateTime.Now}执行了事件处理程序");
}
}
订阅者类中有一个EventHandler方法,与前面定义的委托类型的签名与返回值类型一致。
在看下主函数:
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
//订阅事件
publisher.MyEvent += subscriber.EventHandler;
publisher.DoCount();
}
publisher.MyEvent += subscriber.EventHandler;
就是在订阅事件,对应上面结构图中的事件注册,将subscriber类的EventHandler方法注册到publisher类的MyEvent事件上。
也可以通过:
publisher.MyEvent -= subscriber.EventHandler;
取消订阅事件。
运行结果如下所示:

本示例全部代码如下所示:
internal class Program
{
public delegate void MyDelegate();
public class Publisher
{
public event MyDelegate MyEvent;
public void DoCount()
{
for(int i = 0; i < 3; i++)
{
Task.Delay(3000).Wait();
//确认有方法可以执行
if(MyEvent != null)
{
//触发事件
MyEvent();
}
}
}
}
public class Subscriber
{
public void EventHandler()
{
Console.WriteLine($"{DateTime.Now}执行了事件处理程序");
}
}
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
//订阅事件
publisher.MyEvent += subscriber.EventHandler;
publisher.DoCount();
}
}
以上就根据上面的结构图写出了一个使用事件的示例,但是本示例还有需要改进的地方。
上面我们触发事件检查空值是这样写的:
//确认有方法可以执行
if(MyEvent != null)
{
//触发事件
MyEvent();
}
C# 6.0 引入了空条件操作符之后,现在也可以这样做空值检查:
MyEvent?.Invoke();
同时也不是一上来就检查空值,而是先将MyEvent赋给第二个委托变量localDelegate:
MyDelegate localDelegate = MyEvent;
localDelegate?.Invoke();
这个简单的修改可确保在检查空值和发送通知之间,如果一个不同的线程移除了所有MyEvent订阅者,将不会引发NullReferenceException异常。
标准 .NET 事件模式
以上我们以一个简单的例子介绍了C#中的事件,但是大家可能会觉得有点模式,跟我们平常在winform中使用的事件好像不太一样,那是因为 .NET 框架提供了一个标准模式,接下来我将以winform中的button按钮点击事件为例进行介绍。
页面很简单,只有一个button按钮:

然后button按钮点击事件的代码如下:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Hello World");
}
现在我们再根据下面这张事件结构图,来看一看标准的 .NET 事件模式:

事件注册
打开解决方案中的Form1.Designer.cs文件:

看到button1相关内容:

button1.Click += button1_Click;
就是在订阅事件,对应上面图中的事件注册。
委托类型声明
右键查看定义:
public event EventHandler? Click
{
add => Events.AddHandler(s_clickEvent, value);
remove => Events.RemoveHandler(s_clickEvent, value);
}
发现Click事件中的委托类型是EventHandler,再查看EventHandler的定义:
public delegate void EventHandler(object? sender, EventArgs e);
这一步对应上面事件结构图中的委托类型声明。
EventHandler是 .NET中预定义的委托,专门用来表示不生成数据的事件的事件处理程序方法应有的签名与返回类型。
第一个参数是sender,用来保存触发事件的对象的引用。由于是object?类型,所以可以匹配任何类型的实例。
第二个参数是e,用于传递数据。但是EventArgs类表示包含事件数据的类的基类,并提供用于不包含事件数据的事件的值。也就是说EventArgs设计为不能传递任何数据。它用于不需要传递数据的事件处理程序,通常会被忽略。如果我们想要传递数据,必须声明一个派生自EventArgs的类,使用合适的字段来保存需要传递的数据。
尽管EventArgs类实际上并不传递数据,但它是使用EventHandler委托模式的重要部分。不管参数使用的实际类型是什么,object类和EventArgs类总是基类,这样EventHandler就能提供一个对所有事件和事件处理器都通用的签名,只允许两个参数,而不是各自都有不同签名。
事件声明
public event EventHandler? Click
{
add => Events.AddHandler(s_clickEvent, value);
remove => Events.RemoveHandler(s_clickEvent, value);
}
Click事件在Control类中定义,Button类继承自ButtonBase类,而ButtonBase类继承自Control类。
public event EventHandler? Click;
对应上面结构图中的事件声明。
触发事件的代码
查看Button类的定义,找到OnClick方法的定义:
protected override void OnClick(EventArgs e)
{
Form? form = FindForm();
if (form is not null)
{
form.DialogResult = _dialogResult;
}
// accessibility stuff
AccessibilityNotifyClients(AccessibleEvents.StateChange, -1);
AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);
// UIA events:
if (IsAccessibilityObjectCreated)
{
AccessibilityObject.RaiseAutomationPropertyChangedEvent(UiaCore.UIA.NamePropertyId, Name, Name);
AccessibilityObject.RaiseAutomationEvent(UiaCore.UIA.AutomationPropertyChangedEventId);
}
base.OnClick(e);
}
去掉无关部分,保留相关部分便于理解:
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
}
这里的base指的是Button类的基类ButtonBase类:

再查看ButtonBase类中OnClick方法的定义:
protected override void OnClick(EventArgs e)
{
base.OnClick(e);
OnRequestCommandExecute(e);
}
发现也有一个base.OnClick(e);,这里的base指的是ButtonBase类的基类Control:

再查看Control类中OnClick方法的定义:
/// <summary>
/// Raises the <see cref="Click"/>
/// event.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void OnClick(EventArgs e)
{
((EventHandler?)Events[s_clickEvent])?.Invoke(this, e);
}
终于找到了触发事件的代码。
事件处理程序
这个想必大家并不陌生,双击button按钮就可以看到:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Hello World");
}
这对应上面结构图中的事件处理程序。该事件处理程序方法的签名与返回值类型与EventHandler委托类型一致。
使用泛型版本的标准 .NET事件模式
接下来我会举一个例子,说明如何使用泛型版本的标准 .NET事件模式。
第一步,自定义事件数据类,该类继承自EventArgs类:
public class MyEventArgs : EventArgs
{
public string? Message { get; set; }
public DateTime? Date { get; set; }
}
拥有两个属性Message与Date。
第二步,写发布者类:
public class Publisher
{
public event EventHandler<MyEventArgs>? SendMessageEvent;
public void SendMessage()
{
for(int i = 0; i < 3; i++)
{
Task.Delay(3000).Wait();
MyEventArgs e = new MyEventArgs();
e.Message = $"第{i+1}次触发事件";
e.Date = DateTime.Now;
EventHandler<MyEventArgs>? localEventHandler = SendMessageEvent;
localEventHandler?.Invoke(this, e);
}
}
}
public event EventHandler<MyEventArgs>? SendMessageEvent;
声明了事件。
EventHandler<MyEventArgs>? localEventHandler = SendMessageEvent;
localEventHandler?.Invoke(this, e);
触发了事件。
第三步,写订阅者类:
public class Subscriber
{
public void EventHandler(object? sender,MyEventArgs e)
{
Console.WriteLine($"Received Message:{e.Message} at {e.Date}");
}
}
包含事件处理程序,该方法与EventHandler<MyEventArgs>委托类型的签名与返回值类型一致。
第四步,写主函数:
static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
publisher.SendMessageEvent += subscriber.EventHandler;
publisher.SendMessage();
}
publisher.SendMessageEvent += subscriber.EventHandler;
订阅事件。
运行结果如下所示:

包含了我们自定义的事件数据。
补充
上面说自定义的事件数据类要继承自EventArgs类,但其实在 .NET Core 的模式较为宽松。 在此版本中,EventHandler<TEventArgs> 定义不再要求 TEventArgs 必须是派生自 System.EventArgs 的类。
因此我在.NET 8 版本的示例中去掉继承自EventArgs类,该示例依旧能正常运行。
异步事件订阅者
一个关于异步事件订阅者的例子如下:
// 事件发布者
public class EventPublisher
{
// 定义异步事件
public event Func<string, Task>? MyEvent;
// 触发事件的方法
public async Task RaiseEventAsync(string message)
{
Func<string, Task> localEvent = MyEvent;
await localEvent?.Invoke(message);
}
}
// 异步事件订阅者
public class AsyncEventSubscriber
{
// 处理事件的异步方法
public async Task HandleEventAsync(string message)
{
Console.WriteLine($"Received event with message: {message}");
// 异步操作,例如IO操作、网络请求等
await Task.Delay(3000);
Console.WriteLine("Event handling complete.");
}
}
class Program
{
static async Task Main(string[] args)
{
// 创建事件发布者
var publisher = new EventPublisher();
// 创建异步事件订阅者
var subscriber = new AsyncEventSubscriber();
// 订阅事件
publisher.MyEvent += subscriber.HandleEventAsync;
// 触发事件
await publisher.RaiseEventAsync("Hello, world!");
Console.ReadLine();
}
}
运行结果如下所示:

总结
本文先是介绍了一些C#中事件的相关概念,然后通过几个例子介绍了在C#中如何使用事件。
参考
1、《C#图解教程》
2、《C# 7.0 本质论》
3、[C# 文档 - 入门、教程、参考。 | Microsoft Learn]
由浅入深理解C#中的事件的更多相关文章
- 怎么理解js中的事件委托
怎么理解js中的事件委托 时间 2015-01-15 00:59:59 SegmentFault 原文 http://segmentfault.com/blog/sunchengli/119000 ...
- 【Swing】理解Swing中的事件与线程
talk is cheap , show me the code. Swing中的事件 事件驱动 所有的GUI程序都是事件驱动的.Swing当然也是. GUI程序不同于Command Line程序,一 ...
- 理解JavaScript中的事件轮询
原文:http://www.ruanyifeng.com/blog/2014/10/event-loop.html 为什么JavaScript是单线程 JavaScript语言的一大特点就是单线程,也 ...
- 学习和理解C#中的事件
注:本文系学习笔记. 上一篇文章记录了我对C#中委托的理解.委托实际上是一种类型.可以将一个或多个方法绑定到委托上面,调用委托时,一次执行委托上面绑定的方法.本文要讲述的事件实际上和委托有很深的“感情 ...
- 理解Javascript中的事件绑定与事件委托
最近在深入实践js中,遇到了一些问题,比如我需要为动态创建的DOM元素绑定事件,那么普通的事件绑定就不行了,于是通过上网查资料了解到事件委托,因此想总结一下js中的事件绑定与事件委托. 事件绑定 ...
- 一个demo让你彻底理解Android中触摸事件的分发
注:本文涉及的demo的地址:https://github.com/absfree/TouchDispatch 1. 触摸动作及事件序列 (1)触摸事件的动作 触摸动作一共有三种:ACTION_DOW ...
- 再次理解javascript中的事件
一.事件流的概念 + 事件流描述的是从页面中接收事件的顺序. 二.事件捕获和事件冒泡 + 事件冒泡接收事件的顺序:
- 理解DOM中的事件流
浏览器发展到第四代时(IE4和Netscape Communicator 4),浏览器团队遇到一个很有意思的问题:页面的哪一部分会拥有特定的事件?想象下在一张纸上有一组同心圆,如果你把手指放在圆心上, ...
- 深入理解javascript中的事件循环event-loop
前面的话 本文将详细介绍javascript中的事件循环event-loop 线程 javascript是单线程的语言,也就是说,同一个时间只能做一件事.而这个单线程的特性,与它的用途有关,作为浏览器 ...
- 理解JavaScript中的事件流
原文地址:http://my.oschina.net/sevenhdu/blog/332014 目录[-] 事件冒泡 事件捕获 DOM事件流 当浏览器发展到第四代时(IE4和Netscape Comm ...
随机推荐
- 在 Net7.0 环境下使用 RestSharp 发送 Http(FromBody和FromForm)请求
一.简介 最近,在做一个数据传输的服务,我在一个Worker Service里面需要访问 WebAPI 接口,并传输数据,也可以提交数据.由于第一次使用 RestSharp 发送请求,也遇到了很多问题 ...
- 用go封装一下二级认证功能
用go封装一下二级认证 本篇为用go设计开发一个自己的轻量级登录库/框架吧 - 秋玻 - 博客园 (cnblogs.com)的二级认证业务篇,会讲讲二级认证业务的实现,给库/框架增加新的功能. 源码: ...
- Python面试——基础面试题
文章目录 1.Python 和 Java.PHP.C.C#.C++等其他语言的对比? C语言既有高级语言的特点,又具有汇编语言的特点,它是结构式语言.C语言应用指针:可以直接进行靠近硬件的操作,但是C ...
- Chapter 6. Build Script Basics
Chapter 6. Build Script Basics 6.1. Projects and tasks Everything in Gradle sits on top of two basic ...
- Util应用框架 7.x 来了
什么是Util应用框架? Util是一个.Net平台下的应用框架,旨在提升中小团队的开发能力,由工具类.分层架构基类.Ui组件,配套代码生成模板,权限等组成. Util应用框架 7.x介绍 Util应 ...
- css 10-13
1.背影样式 backgroud-color 背景颜色 backgroud-color :red backgroud-image 背 ...
- 文心一言 VS 讯飞星火 VS chatgpt (114)-- 算法导论10.2 7题
七.用go语言,给出一个 O(n)时间的非递归过程,实现对一个含 n个元素的单链表的逆转.要求除存储链表本身所需的空间外,该过程只能使用固定大小的存储空间. 文心一言: 在Go语言中,你可以使用迭代方 ...
- Python:利用math和random模块实现RSA加密算法
实验五报告: 利用math和random模块实现RSA加密算法 实验目标 本实验的主要目标是熟悉RSA(Rivest-Shamir-Adleman)密码算法的编写,其中包括求最大公因子.模逆的扩展欧几 ...
- python爬虫入门(1)-开发环境配置
所谓的爬虫,就是通过模拟点击浏览器发送网络请求,接收站点请求响应,获取互联网信息的一组自动化程序. 也就是,只要浏览器(客户端)能做的事情,爬虫都能够做. 现在的互联网大数据时代,给予我们的 ...
- Chromium Trace and Perfetto使用详解
1. Trace chromium 在 base 库中提供了 base::trace_event::TraceLog 类,该类是 TRACE_EVENT* , TRACE_COUNTER* 等宏的底层 ...