由浅入深理解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 stuffAccessibilityNotifyClients(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#中的事件
目录 本文较长,给大家提供了目录,可以直接看自己感兴趣的部分。 前言有关事件的概念示例 简单示例 标准 .NET 事件模式 使用泛型版本的标准 .NET 事件模式 补充总结 参考前言 前面介绍了C#中的委托,事件的很多部分都与委托…...
Nginx(十六) 配置文件详解 - server stream服务流
本篇文章主要讲 ngx_stream_core_module 模块下各指令的使用方法,Nginx默认未配置该模块,需要用“--with-stream”配置参数重新编译Nginx。 worker_processes auto;error_log /var/log/nginx/error.log info;events {worker_connections 1024; }stream…...
Css中默认与继承
initial默认样式: initial 用于设置 Css 属性为默认值 h1 {color: initial; }如display或position不能被设置为initial,因为有默认属性。例如:display:inline inherit继承样式: inherit 用于设置 Css 属性应从父元素继承 di…...
gitee上的vue大屏项目
在 Gitee 上,有几个值得注意的 Vue 大屏项目:vue-big-screen-plugin (Gitee): 这是一个基于 Vue3、Typescript、DataV 和 ECharts5 框架的可视化大屏项目。它使用 .vue 和 .tsx 文件构建界面,并采用新版动态屏幕适配方案。这个项目支持数据的动态刷新渲染,内部的 DataV 和 …...
【LeetCode:114. 二叉树展开为链表 | 二叉树 + 递归】
🚀 算法题 🚀 🌲 算法刷题专栏 | 面试必备算法 | 面试高频算法 🍀 🌲 越难的东西,越要努力坚持,因为它具有很高的价值,算法就是这样✨ 🌲 作者简介:硕风和炜,…...
社保养老金发放计算方法
退休后养老金计算公式很复杂,自己自行百度查一下,这里说一下男性,女工人,女干部之间计算差别。 退休后,能到手的养老金多少,取决于你的个人账户里的钱,个人账户里的钱越多,到手养老…...
概率论基础复习题
一、填空题 二、选择题 答案:B 答案:C 答案:C 答案:D。统计量不含任何未知参数。 答案:A 答案:C 样本均值是总体均值的无偏估计;样本方差是总体方差的无偏估计。 答案:B。统计值是一…...
c++,mutex,unique_lock,recursive_mutex,shared_mutex对比分析
当处理多线程并发时,正确使用锁是确保线程安全的关键。 1. std::mutex(互斥锁): std::mutex 是C标准库提供的最基本的锁。它的基本使用如下: #include <iostream> #include <mutex> #include <threa…...
MySQL与Oracle数据库在网络安全等级方面用到的命令
MySQL数据库命令集 查看数据库版本 SELECT VERSION(); 空口令查询 SELECT user,host,account_locked FROM mysql.user WHERE user ; SELECT * FROM mysql.user; 查询 用户的密码加密情况 SELECT HOST,USER,PLUGIN FROM mysql.user; 查询是否有空用户 SELECT host,user,plug…...
MySQL——视图
目录 一.视图介绍 二.基本使用 三.视图规则和限制 一.视图介绍 视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。视图的数据变化会影响到基表,基表的数据变化也会影响到视图。 二.基本使用 创…...
【响应式编程-03】Lambda表达式底层实现原理
一、简要描述 Lambda的底层实现原理Lambda表达式编译和运行过程 二、Lambda的底层实现原理 Lambda表达式的本质 函数式接口的匿名子类的匿名对象 反编译:cfr-0.145.jar 反编译:LambdaMetafactory.metafactory() 跟踪调试,转储Lambda类&#x…...
深入理解可变参数
1.C语言方式 目录 1.C语言方式 1.1.宏介绍 1.2.原理详解 1.3.宏的可变参数 1.4.案例分析 1.5.其他实例 2.C之std::initializer_list 2.1.简介 2.2.原理详解 2.3.案例分析 3.C之可变参数模版 3.1.简介 3.2.可变参数个数 3.3.递归包展开 3.4.逗号表达式展开 3.5…...
Centos7.9和Debian12部署Minio详细流程
一、安装minio Centos wget https://dl.min.io/server/minio/release/linux-amd64/archive/minio-20230227181045.0.0.x86_64.rpm -O minio.rpm sudo dnf install minio.rpmDebian wget https://dl.min.io/server/minio/release/linux-amd64/archive/minio_20230227181045.0…...
软件测试|教你如何使用UPDATE修改数据
简介 在SQL(Structured Query Language)中,UPDATE语句用于修改数据库表中的数据。通过UPDATE语句,我们可以更新表中的特定记录或多条记录,从而实现数据的修改和更新。本文将详细介绍SQL UPDATE语句的语法、用法以及一…...
新闻稿发布:媒体重要还是价格重要
在当今信息爆炸的数字时代,企业推广与品牌塑造不可或缺的一环就是新闻稿发布。新闻稿是一种通过媒体渠道传递企业信息、宣传品牌、事件或产品新闻的文本形式。发布新闻稿的过程旨在将企业的声音传递给更广泛的受众,借助媒体平台实现品牌故事的广泛传播。…...
prometheus grafana mysql监控配置使用
文章目录 前传bitnami/mysqld-exporter:0.15.1镜像出现了问题.my.cnf可以用这个"prom/mysqld-exporter:v0.15.0"镜像重要的事情mysql监控效果外传 前传 prometheus grafana的安装使用:https://nanxiang.blog.csdn.net/article/details/135384541 本文说…...
鸿蒙HarmonyOS-带笔锋手写板(三)
笔者用ArkTS 写了一个简单的带笔锋的手写板应用,并且可以将手写内容保存为图片。 一、效果图 手写效果如下(在鸿蒙手机模拟器上运行,手写时反应可能会有点慢) 二、实现方法 参考文章: 支持笔锋效果的手写签字控件_a…...
React 实现 Step组件
简介 本文将会实现步骤条组件功能。步骤条在以下几个方面改进。 1、将url与Step组件绑定,做到浏览器刷新,不会重定向到Step 1 2、通过LocalStorage 存储之前的Step,做到不丢失数据。 实现 Step.jsx (组件) import {useEffect, useState} fro…...
【OJ】单链表刷题
力扣刷题 1. 反转链表(206)1.1 题目描述1.2 题目分析1.2.1 头插法1.2.2 箭头反转 1.3 题目代码1.3.1 头插入1.3.2 箭头反转 2.合并两个有序链表(21)2.1 题目描述2.2 题目分析2.3 题目代码 1. 反转链表(206)…...
【UML建模】部署图(Deployment Diagram)
1.概述 部署图是一种结构图,用于描述软件系统在不同计算机硬件或设备上的部署和配置情况,以图形化的方式展示系统中组件、节点和连接之间的物理部署关系。 通过部署图,可以清晰地了解系统的物理结构和部署方式,包括系统组件和节…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...
IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)
文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...
技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...
uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
uniapp 字符包含的相关方法
在uniapp中,如果你想检查一个字符串是否包含另一个子字符串,你可以使用JavaScript中的includes()方法或者indexOf()方法。这两种方法都可以达到目的,但它们在处理方式和返回值上有所不同。 使用includes()方法 includes()方法用于判断一个字…...
Visual Studio Code 扩展
Visual Studio Code 扩展 change-case 大小写转换EmmyLua for VSCode 调试插件Bookmarks 书签 change-case 大小写转换 https://marketplace.visualstudio.com/items?itemNamewmaurer.change-case 选中单词后,命令 changeCase.commands 可预览转换效果 EmmyLua…...
论文阅读:Matting by Generation
今天介绍一篇关于 matting 抠图的文章,抠图也算是计算机视觉里面非常经典的一个任务了。从早期的经典算法到如今的深度学习算法,已经有很多的工作和这个任务相关。这两年 diffusion 模型很火,大家又开始用 diffusion 模型做各种 CV 任务了&am…...
2.3 物理层设备
在这个视频中,我们要学习工作在物理层的两种网络设备,分别是中继器和集线器。首先来看中继器。在计算机网络中两个节点之间,需要通过物理传输媒体或者说物理传输介质进行连接。像同轴电缆、双绞线就是典型的传输介质,假设A节点要给…...
boost::filesystem::path文件路径使用详解和示例
boost::filesystem::path 是 Boost 库中用于跨平台操作文件路径的类,封装了路径的拼接、分割、提取、判断等常用功能。下面是对它的使用详解,包括常用接口与完整示例。 1. 引入头文件与命名空间 #include <boost/filesystem.hpp> namespace fs b…...
