由浅入深理解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.概述 部署图是一种结构图,用于描述软件系统在不同计算机硬件或设备上的部署和配置情况,以图形化的方式展示系统中组件、节点和连接之间的物理部署关系。 通过部署图,可以清晰地了解系统的物理结构和部署方式,包括系统组件和节…...
三、计算机理论-关系数据库-数据模型与数据视图;关系代数、关系演算及关系模型
数据模型 具体事物-抽象化-->概念模型-数据化-->数据模型 概念模型也称信息模型,在数据库设计阶段,由设计者按照用户的观点对数据和信息建模,实现对现实世界的概念抽象; 数据模型主要包括网状模型、层次模型、关系模型、面向…...
解读 $mash 通证 “Fair Launch” 规则(Staking 玩法解读篇)
Solmash 是 Solana 生态中由社区主导的铭文资产 LaunchPad 平台,该平台旨在为 Solana 原生铭文项目,以及通过其合作伙伴 SoBit 跨链桥桥接到 Solana 的 Bitcoin 生态铭文项目提供更广泛的启动机会。...
【C语言】关于C11的一些新特性
相比于VC 6.0使用的ANSI C标准,VS2022使用的C11标准与上一代有很多不同,相比之前的 C 标准(如 C89/C90 和 C99),引入了一些新的功能、特性和改进。以下是 C11 标准相对于之前版本的一些主要变化和新增内容:…...
牛的速记(c++题解)
题目描述 奶牛们误解了速记的含义。他们是这样理解的: 给出一个少于255个字母的小写字母串。 找到一个出现次数最多的字母,将该字母从字母串中统统删去,如果出现次数最多的字母不止一个,就删去在字母表中靠前的一个,即…...
使用ffmpeg+flv.js + websokect播放rtsp格式视频流
对于rtsp的视频流网上有很多种的解决方案,但是大的趋势还是利用ffmpeg的工具进行rtsp的视频解析进行一个推流,我最终选择bilibili开源的flv.js,代码十分的简单全部都在底层封装好了。实现的方式也比较容易理解,ffmpeg进行rtsp的视…...
OAI openair3代码结构整理
openair3代码框架结构 OAI(OpenAirInterface)是一个开源的5G网络软件平台,用于研究和开发5G网络技术。OpenAir3是OAI项目中的一个子项目,专注于5G核心网络的功能实现。 一、OpenAir3的代码主要包括以下几个部分: NAS…...
Kubernets(K8S)启动和运行 01-01 Kubernetes简介
Kubernets(K8S)启动和运行 01-01 Kubernetes简介 Kubernetes is an open source orchestrator for deploying containerized applications. It was originally developed by Google, inspired by a decade of experience deploying scalable, reliable systems in containers …...
PHP特性知识点扫盲 - 下篇
概述 在实际的生产环境中遇到了实际需要解决的问题,需要把服务部署的方式梳理出来,在同一个服务器中部署多个PHP环境,架构图如下: 架构方案 在工作实践中遇到的很多问题的普遍性都是相通的,公司运行的可新项目都是版…...
HarmonyOS应用开发之DevEco Studio安装与初次使用
1、DevEco Studio介绍 DevEco Studio是基于IntelliJ IDEA Community开源版本打造,面向华为终端全场景多设备的一站式集成开发环境(IDE),为开发者提供工程模板创建、开发、编译、调试、发布等E2E的HarmonyOS应用/服务的开发工具。…...
记录第一次在GitHub上面提交Issue
第一次在GitHub上面提交Issue,记录一下。 对着源码调了好久才发现,问题并不在程序而在模型(虽然只是一个很小的问题,但是能够解决问题,并且做出了自己的一点小小贡献,还是很开心。嘻嘻,发博客记…...
可以编辑图片的wordpress/渠道推广
linux文件系统中,为什么不能对目录建立硬链接?http://askubuntu.com/questions/210741/why-are-hard-links-not-allowed-for-directories; 一般 hard link 用於目錄目前暫時是無法達成的,一但使用都會出現 operation; not permitted 這類訊息…...
垂直型b2c网站有哪些/网络软营销
文章目录一、引言1.1 数据库压力过大1.2 数据不同步1.3 传统锁失效二、Redis介绍2.1 NoSQL介绍2.2 Redis介绍三、Redis安装3.1 安装Redis3.2 使用redis-cli连接Redis3.3 使用图形化界面连接Redis四、Redis常用命令【重点】4.1 Redis存储数据的结构4.2 string常用命令4.3 hash常…...
网站移动端怎么做/宁波seo排名优化哪家好
JAVA之旅(三)——数组,堆栈内存结构,静态初始化,遍历,最值,选择/冒泡排序,二维数组,面向对象思想我们继续JAVA之旅一.数组1.概念数组就是同一种类型数据的集合,就是一个容器数组的好…...
麻城网站建设/seo效果最好的是
对Activity启动模式的理解 应用场景 在已打开多个Activity应用B的前提下,应用A调用应用B后点击返回按钮,需要直接返回到A应用,而不是打开B应用的上一个Activity一个Task可以理解为一个Activity栈,可以装载一个或多个Activity&…...
大连做网站的公司/太原百度关键词优化
http://www.lydsy.com/JudgeOnline/problem.php?id1037 一个区间是否满足 任意连续的一段,男孩与女孩的数目之差不超过k, 取决于男孩与女孩数目之差的最大值是否满足条件 而且若在一个位置 上坐一个男孩, 所有包含这个位置的区间 男孩与女孩…...
网络营销推广方法选择/网站优化招商
rm -rf ./* cp -ra dir1/. dir2 find / -name main.cpp find . -name ‘*src11*’ // 模糊匹配文件名 find / -name AnmyTest -type d(查找AnmyTest目录位置) -type TYPE: 查找文件类型为TYPE的,类型主要有:一般正规文件(f)、设备文件(b,c)、目录(d)、连接文件(l)、socket(s…...