【C#学习笔记】事件

在这里插入图片描述


前言

在之前我学习委托的时候,写到了

学习了委托,事件其实也就学习了,事件和委托基本上一模一样:

然而在实际工作中通过对事件的深入学习后发现,实际上事件的使用比委托要严格一些,本节将详细讲解事件的使用。

视频参考:【事件•语法篇】如何声明自定义的事件以及事件的完整/简略声明格式


文章目录

  • 事件的定义
    • 事件和事件模型
    • 使用事件的好处
  • 事件的声明格式
    • 事件的完整声明
      • 小结
    • 事件的简略声明
  • 泛型委托定义下的事件
  • 为什么使用事件?


事件的定义

事件(event)有能力使一个类或者对象在发生相关事情的时候去通知其他类,对象们。简单来说一个事件在发生后会去通知所有的监听事件的成员函数,让它们进行对应的事件处理。

乍一看事件和多播委托很像,实际上事件也是委托的一种特殊的封装。

事件和事件模型

在这里插入图片描述
事件模型拥有五大要素,分别是:

  • 事件的拥有者
  • 事件
  • 事件的响应者
  • 事件处理器
  • 事件定义(+=)

五大要素也很好理解,事件的拥有者就是定义事件的类或者对象,事件的响应者就是事件多播时注册处理器Handler方法的那些类或者对象。事件就是指这个特殊的委托封装,事件的处理器就是一种在委托约束下的方法。事件定义就是注册方法的操作符(只能是+=-=)。

事件区别于委托,有一个重要的限制,就是事件Event和事件处理器EventHandler必须属于同一委托类型,如果不是同一委托类型,则事件处理器和事件就是不匹配的。

本质上,事件是基于委托的,一方面,事件的注册需要使用委托类型进行约束,它约束了该事件应该处理什么类型的事件数据EventArgs以保证类型兼容。另一方面,事件中注册的各种Handler的调度也是基于多播委托的。

使用事件的好处

使用事件的好处在于,通过对委托的封装增加了一些更严格的使用规则:例如事件只能放在+=-=的左侧,就避免了对委托直接用=赋值导致整个委托被重置的问题。例如事件必须定义senderFooEventArgs,就方便我们对拥有者以及传递的数据进行适当的处理。


事件的声明格式

.Net中规定,声明事件的委托必须使用EventHandler作为结尾,提高代码可读性。而实际上这个EventHandler也是官方给出的一种标准的委托类型:

public delegate void EventHandler(object sender, EventArgs e);

其中,响应者或者处理者是sender,类型是万物之父object,也就是可接收所有类。数据类型是EventArgs,这是事件的“处理数据”的基类,任何事件中用于传递或处理的数据都必须继承于EventArgs这个基类。同样的,继承于EventArgs类型的处理数据也需要以XXXEventArgs来命名,表示它是XXXEventHandler的事件数据。

使用事件的方法是仿照上述委托类型声明一个全新的事件委托,当然也可以直接使用EventHandler这个事件,但是要避免由于object的类型转换所产生的装箱拆箱,在直接使用EventHandler的时候,如果传入不同类型的sender,为了避免强转使用导致的装箱拆箱,通常用as来进行隐式转换。

事件的完整声明

让我们来写一段完整的自定义事件声明的格式代码,以视频中的代码为例,这个事件的拥有者是客户,事件是一个点单的事件,事件的响应者是服务员,事件处理器是客户的点单事件EventHandler:

// .Net中规定,声明事件的委托必须使用EventHandler作为结尾,提高代码可读性
// 该委托指定了事件的类型约束,其中响应者Sender的类型是Customer,处理数据是OrderEventArgs
public delegate void OrderEventHandler(Customer _customer,OrderEventArgs _e);public class Customer
{public float Bill {get;set;}public void PayTheBill(){Debug.Log("I have to pay:" + this.Bill);}// 定义完整的事件声明格式// 这个orderEventHandler私有委托被封装在public的事件当中,用于限制对委托的访问private OrderEventHandler orderEventHandler;// 定义事件OnOrder,完整声明类似于属性,需要定义基本的添加器和移除器public event OrderEventHandler OnOrder{add{orderEventHandler += value;}remove{orderEventHandler -= value;}}
}// 继承了EventArgs基类的对应事件的处理数据,并定义其内部属性
public class OrderEventArgs : EventArgs
{public string CoffeeName {get;set;}public string CoffeeSize {get;set;}public float CoffeePrice {get;set;}
}

现在,我们已经准备好了一个事件和它的拥有者,接下来需要一个响应者来处理事件。

public class EventEx : MonoBehavior
{Customer customer = new Customer();Waiter waiter = new Waiter();private void Start(){customer.OnOrder += waiter.TakeAction;}
}public class Waiter
{事件响应通过事件传递的事件数据中的咖啡size的类型来判断每个客户的订单应该收什么价格。internal void TakeAction(Customer _customer, OrderEventArgs _e){float finalPrice = 0;switch(_e.CoffeeSize){case "Tall":finalPrice  = _e.CoffeePrice;break;case "Grand":finalPrice  = _e.CoffeePrice + 3;break;case "Venti":finalPrice  = _e.CoffeePrice + 6;break;}_customer.Bill += finalPrice;}
}

最后我们还需要触发这个事件,因此我们在Customer中定义一个Order函数来触发委托。只需要为委托传入类型匹配的参数,即可触发所有绑定的事件处理器EventHandler:

public class EventEx : MonoBehaviour
{Customer customer = new Customer();Waiter waiter = new Waiter();OrderEventArgs e = new OrderEventArgs();private void Start(){customer.OnOrder += waiter.TakeAction;customer.Order();// 输出结果:I have to pay:64customer.PayTheBill();}
}public delegate void OrderEventHandler(Customer _customer, OrderEventArgs _e);public class Customer
{public float Bill { get; set; }public void PayTheBill(){Debug.Log("I have to pay:" + this.Bill);}private OrderEventHandler orderEventHandler;public event OrderEventHandler OnOrder{add{orderEventHandler += value;}remove{orderEventHandler -= value;}}public void Order(){// 为两杯咖啡触发了两次点单事件if(orderEventHandler != null){OrderEventArgs e = new OrderEventArgs();e.CoffeeName = "Mocha";e.CoffeeSize = "Tall";e.CoffeePrice = 28;orderEventHandler(this, e);OrderEventArgs e1 = new OrderEventArgs();e1.CoffeeName = "Latte";e1.CoffeeSize = "Venti";e1.CoffeePrice = 30;orderEventHandler(this, e1);}}
}

小结

小结一下刚才讲的内容:
首先我们应当确定好事件的拥有者和响应者之间的关系,例如顾客和服务员,因为我们需要顾客点单,服务员才会有反应。因此顾客是事件的拥有者,当其点单之后服务员作为响应者去响应这个事件。

然后需要定义事件,在成员外部定义事件的FooEventHandler的委托约束,并定义内部senderFooEventArgs的类型。在事件进行完整定义的时候,需要在成员内部(事件拥有者)定义委托fooEventHandler和事件OnFoo(包括对添加器Add和移除器Remove的定义)。

最后,将事件与响应Handler绑定,想要使用的时候就直接调用即可。


事件的简略声明

通常事件的声明,往往使用更简略的声明方式。简略声明的好处是提供了一些特殊的语法糖。

	public event OrderEventHandler OnOrder;public void Order(){if(OnOrder != null){OrderEventArgs e = new OrderEventArgs();e.CoffeeName = "Mocha";e.CoffeeSize = "Tall";e.CoffeePrice = 28;OnOrder(this, e);OrderEventArgs e1 = new OrderEventArgs();e1.CoffeeName = "Latte";e1.CoffeeSize = "Venti";e1.CoffeePrice = 30;OnOrder.Invoke(this, e1);}}

我们修改一下顾客类中的事件声明和代码。发现几个特点:

  1. OnOrder直接用event关键字声明了一个事件,而不是先声明一个委托,再声明事件中对委托的添加器和移除器的定义。
  2. Order直接用!=来比较委托是否为空,我们说事件的操作符只能是-=+=,在此处却可以使用!=甚至=(仅限成员函数内部),这也是迫不得已,因为我们没有定义委托,所以直接用事件来代替委托进行操作。然而委托真的没有被定义吗?只是编译器内部帮我们定义好了一个委托,我们看不到而已。
  3. 在触发事件的时候,不仅用通常的方法OnOrder(this, e);来触发,还可以使用OnOrder?.Invoke(this, e1);OnOrder.Invoke(this, e1);来进行触发,更加灵活了。

从上述代码来看,简略声明的事件更灵活,更强大。

此外,由于简略声明事件的定义格式public event OrderEventHandler OnOrder;,不要误以为它是一个字段,只是语法糖的存在让它看起来长得像一个字段。实际上还是一个事件。


泛型委托定义下的事件

除了常态的委托类型之外,定义事件我们也可以用到泛型委托,例如微软官方提供的泛型委托:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

所以我们也可以定义一个泛型委托,例如不止顾客点单,服务员自己也可以给自己点咖啡,在不继承同一个基类的情况下就可以用泛型委托来接受不同类型对象的事件响应。

public delegate void OrderEventHandler<Tsender>(Tsender sender, OrderEventArgs _e);

为什么使用事件?

如果我们将下列事件中的event关键字去掉,可以正常处理上述代码吗?答案是可以

public event OrderEventHandler OnOrder;
//变成了委托
public  OrderEventHandler OnOrder;

既然如此,我们为什么要使用事件呢?
因为委托的封装不够严密,不符合我们对于事件的想象。我们可以用如下方式去访问类中public的委托:

customer1.OnOrder(customer1,e1);
customer2.OnOrder(customer1,e2);

在上述代码中,顾客1为自己点了一份名为e1的订单,这是没有问题的。
但是顾客2也为顾客1点了一份名为e2的订单,顾客2直接访问了顾客1中public出来的委托字段,一般而言,我们不希望通过这样的方式去为其他类触发事件。这会造成一些逻辑上的错误。使用事件,就可以把其对应的委托封装起来,避免一些奇怪的用法。

事件的存在就是为了阻止一些委托调度的“非法操作”,更安全,更有约束。

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

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

相关文章

深入理解Python迭代器与生成器

文章目录 1. 迭代器协议代码示例:2. 生成器基础代码示例:3. 使用yield的高级技巧代码示例:4. 生成器表达式代码示例:迭代器和生成器是Python中实现迭代的两种主要方式,它们都允许用户创建可以遍历数据集的对象。在Python中,迭代器协议是指对象需要遵守__iter__()和__next…

YOLOv5论文作图教程(2)— 软件界面布局和基础功能介绍

前言:Hello大家好,我是小哥谈。通过上一节课的学习,相信大家都已成功安装好软件了,本节课就给大家详细介绍一下Axure RP9软件的界面布局及相关基础功能,希望大家学习之后能够有所收获!🌈 前期回顾: YOLOv5论文作图教程(1)— 软件介绍及下载安装(包括软件包+下载安…

数据仓库工具箱-零售业务

文章目录 一、维度模型设计的4步过程1.1 第一步&#xff1a;选择业务过程1.2 第二步&#xff1a;声明粒度1.3 第三步&#xff1a;确定维度1.4 第四步&#xff1a;确定事实 二、零售业务案例研究2.1 第一步&#xff1a;选择业务过程2.2 第二步&#xff1a;声明粒度2.3 第三步&am…

2022最新版-李宏毅机器学习深度学习课程-P34 自注意力机制类别总结

在课程的transformer视频中&#xff0c;李老师详细介绍了部分self-attention内容&#xff0c;但是self-attention其实还有各种各样的变化形式&#xff1a; 一、Self-attention运算存在的问题 在self-attention中&#xff0c;假设输入序列&#xff08;query&#xff09;长度是N…

Unity Input System最简单使用

开始学的是 Input Manager 比较好理解&#xff0c;Input System却不好理解&#xff0c;教程也找了很多&#xff0c;感觉都讲的不清楚&#xff0c;我这里做一个最简单的用 Input System 添加鼠标左键和右键的效果。 1. 安装 Input System 包 首先这个功能不是内置的&#xff0…

MATLAB画图由于线段太多导致导出图片模糊的解决办法

Matlab画图如果figure内的线条过多&#xff0c;或者散点过多&#xff0c;导出的图片会模糊&#xff0c;解决方案 解决方法就在于figure的导出设置中。 在设置的渲染选项中&#xff0c;渲染器有两个&#xff0c;分别为painters和OpenGL&#xff0c;分别为矢量格式输出和位图输出…

【mongoose】mongoose 基本使用

1. 连接数据库 // 1. 安装 mongoose // 2. 导入 mongoose const mongoose require(mongoose) // 3. 连接 mongodb 服务 mongoose.connect(mongodb://127.0.0.1:27017/xx_project) // 4. 设置回调 .on 一直重复连接 .once 只连接一次 mongoose.connection.on(open, () >…

STA——绪论

一、概述 静态时序分析&#xff08;简称STA&#xff09;是用来验证数字设计时序的技术之一&#xff0c;另外一种验证时序的方法是时序仿真&#xff0c;时序仿真可以同时验证功能和时序。“时序分析”这个术语就是用来指代“静态时序分析“或”时序仿真“这两种方法之一&#xf…

MapReduce性能优化之小文件问题和数据倾斜问题解决方案

文章目录 MapReduce性能优化小文件问题生成SequenceFileMapFile案例 &#xff1a;使用SequenceFile实现小文件的存储和计算 数据倾斜问题实际案例 MapReduce性能优化 针对MapReduce的案例我们并没有讲太多&#xff0c;主要是因为在实际工作中真正需要我们去写MapReduce代码的场…

【江协科技-用0.96寸OLED播放知名艺人打篮球视频】

Python进行视频图像处理&#xff0c;通过串口发送给stm32&#xff0c;stm32接收数据&#xff0c;刷新OLED进行显示。 步骤&#xff1a; 1.按照接线图连接好硬件 2.把Keil工程的代码下载到STM32中 3.运行Python代码&#xff0c;通过串口把处理后的数据发送给STM32进行显示 …

阿里云99元服务器2核2G3M带宽_4年396元_新老用户均可

阿里云2核2G3M带宽99元服务器新老用户同享&#xff0c;续费不涨价&#xff0c;99元即可续费&#xff0c;可以续费到2027年&#xff0c;相当于396元买4年&#xff0c;阿里云百科aliyunbaike.com来详细说下阿里云99元服务器配置、购买条件、优惠价格和续费攻略&#xff1a; 阿里…

你是怎么理解自动化测试的?理解自动化测试的目的和本质

其实自动化测试很好理解&#xff0c;由两部分组成&#xff0c;“自动化”和“测试”&#xff0c;所以我们要理解自动化测试&#xff0c;就必须理解“自动化”和“测试”&#xff0c;只有理解了这些概念&#xff0c;才能更轻松的做好的自动化测试。其中“自动化”可以想象成通过…

服务号升级为订阅号的方法

服务号和订阅号有什么区别&#xff1f;服务号转为订阅号有哪些作用&#xff1f;我们都知道&#xff0c;服务号一个月只能发4次文章&#xff0c;但是订阅号每天都能发文章。不过在接收消息这一方面&#xff0c;服务号群发的消息有消息提醒&#xff0c;并显示在对话框&#xff1b…

word办公小技巧:方框打勾、上下标、横隔线、排序

Word文件制作过程中&#xff0c;需要了解一些可以提高效率的小技巧帮助我们能够更快的完成工作&#xff0c;今天分享四个提高效率的小技巧 技巧一&#xff1a;方框内打√ 想要在word文件中设置出方框内√&#xff0c;的效果&#xff0c;在word文件中输入&#xff1a; ☑&…

【Hugging Face】如何下载模型文件

参考文章&#xff1a; 1、mac安装Homebrew - 知乎 2、 ssh连接 git lfs install git clone githf.co:bert-base-uncased -- 安装Homebrew /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" -- 配置文件生效 source /Use…

pytorch_神经网络构建5

文章目录 生成对抗网络自动编码器变分自动编码器重参数GANS自动编码器变分自动编码器gans网络Least Squares GANDeep Convolutional GANs 生成对抗网络 这起源于一种思想,假如有一个生成器,从原始图片那里学习东西,一个判别器来判别图片是真实的还是生成的, 假如生成的东西能以…

渗透测试学习day2

文章目录 连接靶机靶机&#xff1a;Fawn 解题过程Task 1Task 2Task 3Task 4Task 5Task 6Task 7Task 8Task 9Task 10Task 11Task 12 总结 连接靶机 详细过程可参考day1 靶机&#xff1a;Fawn 难度&#xff1a;very easy &#xff08;ftp服务的靶机&#xff09; 解题过程 T…

时间序列预测模型实战案例(十)(CNN-GRU-LSTM)通过堆叠CNN、GRU、LSTM实现多元预测和单元预测

本文介绍 本篇博客为大家讲解的是通过组堆叠CNN、GRU、LSTM个数&#xff0c;建立多元预测和单元预测的时间序列预测模型&#xff0c;其效果要比单用GRU、LSTM效果好的多&#xff0c;其结合了CNN的特征提取功能、GRU和LSTM用于处理数据中的时间依赖关系的功能。通过将它们组合在…

合肥工业大学数字逻辑实验三

** 数字逻辑 实验报告** ✅作者简介:CSDN内容合伙人、信息安全专业在校大学生🏆 🔥系列专栏 :hfut实验课设 📃新人博主 :欢迎点赞收藏关注,会回访! 💬舞台再大,你不上台,永远是个观众。平台再好,你不参与,永远是局外人。能力再大,你不行动,只能看别人成功!…

yolov8+多算法多目标追踪+实例分割+目标检测+姿态估计(代码+教程)

多目标追踪实例分割目标检测 YOLO (You Only Look Once) 是一个流行的目标检测算法&#xff0c;它能够在图像中准确地定位和识别多个物体。 本项目是基于 YOLO 算法的目标跟踪系统&#xff0c;它将 YOLO 的目标检测功能与目标跟踪技术相结合&#xff0c;实现了实时的多目标跟…