百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

由浅入深理解C#中的事件(c#事件的定义)

yuyutoo 2025-03-30 22:52 5 浏览 0 评论

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

前言

有关事件的概念

示例

简单示例

标准 .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 != )
{
//触发事件
MyEvent();
}

}
}
}

事件声明:

 public event MyDelegate MyEvent;

事件声明在一个类中,它需要委托类型的名称,任何注册到事件的处理程序都必须与委托类型的签名和返回类型匹配。它声明为public,这样其他类和结构可以在它上面注册事件处理程序。不能使用对象创建表达式(new表达式)来创建它的对象。

一个常见的误解就是把事件认为是类型,事件其实不是类型,它和方法、属性一样是类或结构的成员。

由于事件是成员,所以我们不能在一段可执行的代码中声明事件,它必须声明在类或结构中,和其他成员一样。

事件成员被隐式自动初始化为。

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

触发事件:

 //确认有方法可以执行
if(MyEvent != )
{
//触发事件
MyEvent();
}

也可以这样写:

 //确认有方法可以执行
if(MyEvent != )
{
//触发事件
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 != )
{
//触发事件
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 != )
{
//触发事件
MyEvent();
}

C# 6.0 引入了空条件操作符之后,现在也可以这样做空值检查:

 MyEvent?.Invoke();

同时也不是一上来就检查空值,而是先将MyEvent赋给第二个委托变量localDelegate:

 MyDelegate localDelegate = MyEvent;
localDelegate?.Invoke();

这个简单的修改可确保在检查空值和发送通知之间,如果一个不同的线程移除了所有MyEvent订阅者,将不会引发ReferenceException异常。

标准 .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 )
{
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方法的定义:

 /// 
/// Raises the
/// event.
///

[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? 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? localEventHandler = SendMessageEvent;
localEventHandler?.Invoke(this, e);
}
}
}
public event EventHandler? SendMessageEvent;

声明了事件。

 EventHandler? 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委托类型的签名与返回值类型一致。

第四步,写主函数:

 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 必须是派生自 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


相关推荐

ETCD 故障恢复(etc常见故障)

概述Kubernetes集群外部ETCD节点故障,导致kube-apiserver无法启动。...

在Ubuntu 16.04 LTS服务器上安装FreeRADIUS和Daloradius的方法

FreeRADIUS为AAARadiusLinux下开源解决方案,DaloRadius为图形化web管理工具。...

如何排查服务器被黑客入侵的迹象(黑客 抓取服务器数据)

---排查服务器是否被黑客入侵需要系统性地检查多个关键点,以下是一份详细的排查指南,包含具体命令、工具和应对策略:---###**一、快速初步检查**####1.**检查异常登录记录**...

使用 Fail Ban 日志分析 SSH 攻击行为

通过分析`fail2ban`日志可以识别和应对SSH暴力破解等攻击行为。以下是详细的操作流程和关键分析方法:---###**一、Fail2ban日志位置**Fail2ban的日志路径因系统配置...

《5 个实用技巧,提升你的服务器安全性,避免被黑客盯上!》

服务器的安全性至关重要,特别是在如今网络攻击频繁的情况下。如果你的服务器存在漏洞,黑客可能会利用这些漏洞进行攻击,甚至窃取数据。今天我们就来聊聊5个实用技巧,帮助你提升服务器的安全性,让你的系统更...

聊聊Spring AI Alibaba的YuQueDocumentReader

序本文主要研究一下SpringAIAlibaba的YuQueDocumentReaderYuQueDocumentReader...

Mac Docker环境,利用Canal实现MySQL同步ES

Canal的使用使用docker环境安装mysql、canal、elasticsearch,基于binlog利用canal实现mysql的数据同步到elasticsearch中,并在springboo...

RustDesk:开源远程控制工具的技术架构与全场景部署实战

一、开源远程控制领域的革新者1.1行业痛点与解决方案...

长安汽车一代CS75Plus2020款安装高德地图7.5

不用破解原车机,一代CS75Plus2020款,安装车机版高德地图7.5,有红绿灯读秒!废话不多讲,安装步骤如下:一、在拨号状态输入:在电话拨号界面,输入:*#518200#*(进入安卓设置界面,...

Zookeeper使用详解之常见操作篇(zookeeper ui)

一、Zookeeper的数据结构对于ZooKeeper而言,其存储结构类似于文件系统,也是一个树形目录服务,并通过Key-Value键值对的形式进行数据存储。其中,Key由斜线间隔的路径元素构成。对...

zk源码—4.会话的实现原理一(会话层的基本功能是什么)

大纲1.创建会话...

Zookeeper 可观测性最佳实践(zookeeper能够确保)

Zookeeper介绍ZooKeeper是一个开源的分布式协调服务,用于管理和协调分布式系统中的节点。它提供了一种高效、可靠的方式来解决分布式系统中的常见问题,如数据同步、配置管理、命名服务和集群...

服务器密码错误被锁定怎么解决(服务器密码错几次锁)

#服务器密码错误被锁定解决方案当服务器因多次密码错误导致账户被锁定时,可以按照以下步骤进行排查和解决:##一、确认锁定状态###1.检查账户锁定状态(Linux)```bash#查看账户锁定...

zk基础—4.zk实现分布式功能(分布式zk的使用)

大纲1.zk实现数据发布订阅...

《死神魂魄觉醒》卡死问题终极解决方案:从原理到实战的深度解析

在《死神魂魄觉醒》的斩魄刀交锋中,游戏卡死犹如突现的虚圈屏障,阻断玩家与尸魂界的连接。本文将从技术架构、解决方案、预防策略三个维度,深度剖析卡死问题的成因与应对之策,助力玩家突破次元壁障,畅享灵魂共鸣...

取消回复欢迎 发表评论: