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

news2025/5/23 9:40:38

目录

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

前言

有关事件的概念

示例

​   简单示例

​   标准 .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 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类:

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.coloradmin.cn/o/1356146.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL——事物

目录 一.发现问题 二.什么时事物 三.事务提交方式 四.事物的常规操作方式 五. 事务隔离级别 1.如何理解隔离性 2.隔离级别 3.查看与设置隔离性 4.读未提交【Read Uncommitted】 5.读提交【Read Committed】 6.可重复读【Repeatable Read】 7.串行化【serializabl…

A01、class文件简要说明

关于 .class 文件的相关结构说明 1、字节码文件相关名词 类型名称数量u4magic1u2minor_version1u2major_version1u2constant_pool_count1cp_infoconstant_poolconstant_pool_count - 1u2access_flags1u2this_class1u2super_class1u2interfaces_count1u2interfacesinterfaces_c…

vue登录 滑动验证,记住密码及AES加密解密

相关依赖 npm install js-cookie //js-cookie npm install crypto-js //AES加密解密 npm install -S vue-puzzle-vcode //滑动验证 vue2 <template><div class"login"><div class"login-box"><div class"imgbox">&…

进阶学习——Linux系统——程序和进程

目录 一、程序和进程的关系 1.程序 2.进程 2.1线程 2.2协程 3.进程与线程的区别 4.总结 4.1延伸 5.进程使用内存的问题 5.1内存泄漏——Memory Leak 5.2内存溢出——Memory Overflow 5.3内存不足——OOM&#xff08;out of memory&#xff09; 5.4进程使用内存出现…

听GPT 讲Rust源代码--compiler(11)

File: rust/compiler/rustc_mir_transform/src/simplify.rs 在Rust源代码中&#xff0c;rust/compiler/rustc_mir_transform/src/simplify.rs文件是Rust编译器中一系列进行MIR&#xff08;中间表示&#xff09;简化的转换的实现。MIR是Rust编译器中用于进行优化和代码生成的中间…

PDF.js实现搜索多个不同的关键词高亮显示效果

static\PDF\web\viewer.js 392行左右 // 自定义搜索关键词---------------------------------------- this.searchKeywords keyword > {if (typeof PDFViewerApplication ! undefined) {PDFViewerApplication.eventBus.dispatch(find, {query: keyword,caseSensitive: fal…

HTML5+CSS3⑥——CSS三大特性、表格、列表

CSS特性 继承性 层叠性 优先级 叠加计算规则 表格 表格结构标签 合并单元格 列表 无序列表 有序列表 定义列表

「数据结构」八大排序1

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;初阶数据结构 &#x1f387;欢迎点赞收藏加关注哦&#xff01; 文章目录 &#x1f349;插入排序&#x1f34c;直接插入排序&#x1f95d;复杂度及稳定性 &#x1f34c;希尔排序&#x1f95d;预…

Required request body is missing报错及解决

今天&#xff0c;我在尝试调用后端接口展示文章数据时遇到了错误&#xff0c;错误原因是请求体缺失&#xff0c; 但是我明明传了参数 然后我找了很久错误原因&#xff0c;发现在之前跟着写的一个差不多的功能时&#xff0c;请求方式是post 而我写的确是get 将get改为post后&…

【DevOps-06】Jenkins实现CI/CD操作

一、简要说明 基于Jenkins拉取GitLab的SpringBoot代码进行构建发布到测试环境实现持续集成 基于Jenkins拉取GitLab指定发行版本的SpringBoot代码进行构建发布到生产环境实现CD实现持续部署 二、准备Springboot工程 1、IDEA新建工程 2、填写项目工程信息 3、选择Springboot版本…

【MATLAB】EEMD_LSTM神经网络时序预测算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~也可转原文链接获取~ 1 基本定义 EEMD-LSTM神经网络时序预测算法是一种结合了扩展经验模态分解&#xff08;EEMD&#xff09;和长短期记忆神经网络&#xff08;LSTM&#xff09;的时间序列预测方法。 EEMD是一种改进的EM…

Numpy基础

目录&#xff1a; 一、简介:二、array数组ndarray&#xff1a;1.array( )创建数组&#xff1a;2.数组赋值和引用的区别&#xff1a;3.arange( )创建区间数组&#xff1a;4.linspace( )创建等差数列&#xff1a;5.logspace( )创建等比数列&#xff1a;6.zeros( )创建全0数组&…

云消息队列 Kafka 版生态谈第一期:无代码转储能力介绍

作者&#xff1a;娜米 云消息队列 Kafka 版为什么需要做无代码转储 云消息队列 Kafka 版本身是一个分布式流处理平台&#xff0c;具有高吞吐量、低延迟和可扩展性等特性。它被广泛应用于实时数据处理和流式数据传输的场景。然而&#xff0c;为了将云消息队列 Kafka 版与其他数…

C++第四天

定义一个Person类&#xff0c;私有成员int age&#xff0c;string &name&#xff0c;定义一个Stu类&#xff0c;包含私有成员double *score&#xff0c;写出两个类的构造函数、析构函数、拷贝构造和拷贝赋值函数&#xff0c;完成对Person的运算符重载(算术运算符、条件运算…

使用Redis进行搜索

文章目录 构建反向索引 构建反向索引 在Begin-End区域编写 tokenize(content) 函数&#xff0c;实现文本标记化的功能&#xff0c;具体参数与要求如下&#xff1a; 方法参数 content 为待标记化的文本&#xff1b; 文本标记的实现&#xff1a;使用正则表达式提取全小写化后的…

特斯拉被比亚迪拉下神坛,马斯克难改命运岁月不如人

当这个消息浮现在我面前时&#xff0c;我几乎不敢相信眼前所见。比亚迪这个我曾无数次在文章中提及的中国车企&#xff0c;终于在2023年完成了伟大的历史任务——以销量突破特斯拉&#xff0c;站上全球电动车王座的宝座&#xff01; 过去我们都习惯于从欧美日韩那里听说汽车的成…

Go 编程必备:bufio 库的全面指南与实战技巧

Go 编程必备&#xff1a;bufio 库的全面指南与实战技巧 引言bufio 库概览bufio.Readerbufio.Writerbufio.Scanner 深入 bufio.Readerbufio.Reader 的基本使用高级功能应用场景 探索 bufio.Writerbufio.Writer 的基本使用高级功能应用场景 运用 bufio.Scannerbufio.Scanner 的基…

钉钉小程序生态7—企业机器人加互动卡片,改善用户体验的开始!

文章导航 钉钉小程序生态1—区分企业内部应用、第三方企业应用、第三方个人应用 钉钉小程序生态2—区分小程序和H5微应用 钉钉小程序生态3—钉钉扫码登录PC端网站 钉钉小程序生态4—钉钉小程序三方企业应用事件与回调 钉钉小程序生态5—钉钉群机器人消息通知和钉钉工作通知 钉…

React Admin 前端脚手架之ant-design-pro

文章目录 一、React Admin 前端脚手架选型二、React Admin 前端脚手架之ant-design-pro三、ant-design-pro使用步骤四、常用总结&#xff08;持续更新&#xff09;EditableProTable组件 常用组件EditableProTable组件 编辑某行后&#xff0c;保存时候触发发送请求EditableProTa…

Bayes贝叶斯识别Spam Email垃圾邮件

目录 介绍&#xff1a; 一、Gaussian Naive Bayes(连续型变量) 1.1数据处理 1.2建模 1.3cross_val_score函数评估 1.4classification_report函数评估 1.5classification_report函数和cross_val_score函数的区别 二、 Multinomial Naive Bayes&#xff08;离散型变量&…