由浅入深理解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.概述 部署图是一种结构图,用于描述软件系统在不同计算机硬件或设备上的部署和配置情况,以图形化的方式展示系统中组件、节点和连接之间的物理部署关系。 通过部署图,可以清晰地了解系统的物理结构和部署方式,包括系统组件和节…...
<6>-MySQL表的增删查改
目录 一,create(创建表) 二,retrieve(查询表) 1,select列 2,where条件 三,update(更新表) 四,delete(删除表…...

Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...
【SpringBoot自动化部署】
SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一,能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时,需要添加Git仓库地址和凭证,设置构建触发器(如GitHub…...

【iOS】 Block再学习
iOS Block再学习 文章目录 iOS Block再学习前言Block的三种类型__ NSGlobalBlock____ NSMallocBlock____ NSStackBlock__小结 Block底层分析Block的结构捕获自由变量捕获全局(静态)变量捕获静态变量__block修饰符forwarding指针 Block的copy时机block作为函数返回值将block赋给…...

ArcGIS Pro+ArcGIS给你的地图加上北回归线!
今天来看ArcGIS Pro和ArcGIS中如何给制作的中国地图或者其他大范围地图加上北回归线。 我们将在ArcGIS Pro和ArcGIS中一同介绍。 1 ArcGIS Pro中设置北回归线 1、在ArcGIS Pro中初步设置好经纬格网等,设置经线、纬线都以10间隔显示。 2、需要插入背会归线…...
大数据驱动企业决策智能化的路径与实践
📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 一、引言:数据驱动的企业竞争力重构 在这个瞬息万变的商业时代,“快者胜”的竞争逻辑愈发明显。企业如何在复杂环…...
Netty自定义协议解析
目录 自定义协议设计 实现消息解码器 实现消息编码器 自定义消息对象 配置ChannelPipeline Netty提供了强大的编解码器抽象基类,这些基类能够帮助开发者快速实现自定义协议的解析。 自定义协议设计 在实现自定义协议解析之前,需要明确协议的具体格式。例如,一个简单的…...