由浅入深理解C#中的事件

目录

本文较长,给大家提供了目录,可以直接看自己感兴趣的部分。

前言有关事件的概念示例​   简单示例​   标准 .NET 事件模式​   使用泛型版本的标准 .NET 事件模式​   补充总结
参考

前言

前面介绍了C#中的委托,事件的很多部分都与委托类似。实际上,事件就像是专门用于某种特殊用途的简单委托,事件包含了一个私有的委托,如下图所示:

image-20240102160538415

有关事件的私有委托需要了解的重要事项如下:

1、事件提供了对它的私有控制委托的结构化访问。我们无法直接访问该委托。

2、事件中可用的操作比委托要少,对于事件我们只可以添加、删除或调用事件处理程序。

3、事件被触发时,它调用委托来依次调用调用列表中的方法。

有关事件的概念

发布者(Publisher):发布某个事件的类或结构,其他类可以在该事件发生时得到通知。

订阅者(Subscriber):注册并在事件发生时得到通知的类或结构。

事件处理程序(event handler):由订阅者注册到事件的方法,在发布者触发事件时执行。

触发(raise)事件:调用(invoke)或触发(fire)事件的术语。当事件触发时,所有注册到它的方法都会被依次调用。

示例

简单示例

现在我们先来看一下最最原始的事件示例。其结构如下所示:

image-20240103101447689

委托类型声明:事件和事件处理程序必须有共同的签名和返回类型,它们通过委托类型进行描述。

事件处理程序声明:订阅者类中会在事件触发时执行的方法声明。它们不一定有显示命名的方法,还可以是匿名方法或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。

事件声明的图解如下所示:

image-20240103140544886

触发事件:

              //确认有方法可以执行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;

取消订阅事件。

运行结果如下所示:

image-20240103151109073

本示例全部代码如下所示:

 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按钮:

image-20240104093125527

然后button按钮点击事件的代码如下:

 private void button1_Click(object sender, EventArgs e){MessageBox.Show("Hello World");}

现在我们再根据下面这张事件结构图,来看一看标准的 .NET 事件模式:

image-20240103101447689

事件注册

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

image-20240104093502598

看到button1相关内容:

image-20240104093620010

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类:

image-20240104103143021

再查看ButtonBase类中OnClick方法的定义:

 protected override void OnClick(EventArgs e){base.OnClick(e);OnRequestCommandExecute(e);}

发现也有一个base.OnClick(e);,这里的base指的是ButtonBase类的基类Control

image-20240104103450257

再查看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;

订阅事件。

运行结果如下所示:

image-20240104115222746

包含了我们自定义的事件数据。

补充

上面说自定义的事件数据类要继承自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();}
}

运行结果如下所示:

image-20240104123351663

总结

本文先是介绍了一些C#中事件的相关概念,然后通过几个例子介绍了在C#中如何使用事件。

参考

1、《C#图解教程》

2、《C# 7.0 本质论》

3、[C# 文档 - 入门、教程、参考。 | Microsoft Learn](

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/325879.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

问题 E: 便利店

题目描述 天宝来到便利店想买些饮料。便利店有各种型号的瓶装饮料售卖&#xff0c;不同型号的饮料卖不同的价格。1瓶0.25升的卖A元&#xff0c;1瓶0.5升的饮料卖B元&#xff0c;1瓶1升的卖C元&#xff0c;1瓶2升的卖D元。便利店里每种饮料都是无限供应。 天宝要买N升的饮料&a…

Beauty algorithm(一) 关键点检测

在实现美妆算法过程中&#xff0c;需要定位到目标区域&#xff0c;常规的图像处理技术很难准确定位。随着AI技术快速发展&#xff0c;人脸关键点成为面部特征提取技术之一。 常见开源的关键点有dlib、mtcnn等。 由于本人对dlib有所了解&#xff0c;且支持C、python等。故本人选…

【深度学习】SDXL tensorRT 推理

stabilityai/stable-diffusion-xl-1.0-tensorrt 项目&#xff1a;https://huggingface.co/stabilityai/stable-diffusion-xl-1.0-tensorrt TensorRT环境&#xff1a; git clone https://github.com/rajeevsrao/TensorRT.git cd TensorRT git checkout release/9.2stabilitya…

智云影院CMS程序PHP源码V3.0 无需数据库

本程序无需数据库&#xff0c;直接上传源码即可访问&#xff0c;&#xff08;服务器或虚拟主机空间&#xff09;都可以搭建使用&#xff01;模板自适应端&#xff0c;浏览体验更佳&#xff01;安装操作简单&#xff01;无需繁琐的操作&#xff0c;即可快速拥有一个视频看片资源…

DMX512输出协议详解

目录 ​编辑 1、DMX512协议简介 2、DMX512协议分析 DMX512指令帧介绍 DMX512信息包 3、DMX512接口电路 4、参考代码 1、DMX512协议简介 DMX512是一种用于舞台灯光控制的数字传输协议。它是由美国舞台灯光协会&#xff08;USITT&#xff09;于1990年发布的工业标准&…

uniapp 日历组件

我们的需求是显示当前月和下个月的排班表 引入 uniapp 日历组件 uni-calendar 做法有两种&#xff0c;一种是直接去修改组件&#xff0c;还有就是文档中提供的 selected 方法 修改组件的就不写了 <uni-calendar :lunar"true" :selected"selected" :in…

助力实体店数字化升级,VR智慧门店打造线上逛店体验

近年来&#xff0c;传统实体店业绩增长过于缓慢&#xff0c;实体门店的销售疲态十分明显&#xff0c;甚至于部分城市已经出现大量线下实体店开始关门的现象&#xff0c;因此顺应实体零售数字化升级趋势已经刻不容缓。越来越多的实体门店开始意识到这个问题&#xff0c;并逐步开…

日程安排小程序实战教程

日常中我们经常有一些事情需要提醒自己&#xff0c;使用日历的形式比较符合实际的使用习惯。本篇我们就利用微搭低代码工具带着大家开发一款日程安排的小程序。 1 创建数据源 登录微搭低代码控制台&#xff0c;打开数据模型&#xff0c;点击创建 输入数据源的名称日程安排 …

MySQL进阶篇(二) 索引

一、索引概述 1. 介绍 索引&#xff08;index&#xff09;是帮助 MySQL 高效获取数据 的 数据结构&#xff08;有序&#xff09;。在数据之外&#xff0c;数据库系统还维护着满足特定查找算法的数据结构&#xff0c;这些数据结构以某种方式引用&#xff08;指向&#xff09;数…

SpringBoot内嵌的Tomcat启动过程以及请求

1.springboot内嵌的tomcat的pom坐标 启动后可以看到tomcat版本为9.0.46 2.springboot 内嵌tomcat启动流程 点击进入SpringApplication.run()方法里面 看这次tomcat启动相关的核心代码refreshContext(context);刷新上下文方法 public ConfigurableApplicationContext run(Stri…

spark的任务提交方式及流程

本地模式 local 测试用,不多赘述 分布式模式 standalone standalone集群是spark 自带的一个资源调度集群&#xff0c;分为两个角色&#xff0c;master/worker&#xff0c;master负责接收任务请求、资源调度&#xff08;监听端口7077&#xff09;&#xff0c;worker负责运行exec…

win10报错“zlib.dll文件丢失,软件无法启动”,修复方法,亲测有效

zlib.dll文件是一个由Zlib创建的动态链接库文件&#xff0c;它是用于Windows操作系统的数据压缩和解压缩的软件。Zlib是一个广泛使用的软件库&#xff0c;广泛应用在许多不同类型的软件中&#xff0c;包括游戏、浏览器和操作系统。 zlib.dll的主要作用是提供数据压缩和解压缩的…