【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…

使用命令进行把新代码上传到git上

步骤1&#xff1a;创建远程仓库 首先&#xff0c;我们需要在一个远程Git仓库中保存我们的代码。这可以是像GitHub、GitLab或Gitee这样的托管服务&#xff0c;也可以是您自己搭建的Git服务器。 在您选择的托管服务上创建一个新的空白仓库。获得远程仓库的URL&#xff0c;因为您…

20个CSS面试题和答案的示例

什么是盒模型&#xff1f;它有哪些部分组成&#xff1f; 答&#xff1a;盒模型是指在网页布局过程中&#xff0c;每个元素都被描绘成一个矩形框&#xff0c;这个矩形框由内容区、内边距、边框和外边距组成。 请解释CSS的层叠顺序&#xff08;Specificity&#xff09;是什么&am…

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; 阿里…

5.vue3项目(五):实现顶部导航栏功能:导航栏静态搭建,菜单折叠功能实现,面包屑动态展示路径,刷新页面功能,全屏功能

目录 一、左侧菜单栏刷新,不要合并菜单 二、顶部tabbar静态搭建 1.新建文件 2.编辑页面 3.结果测试

数字摘要的概念和应用(文件完整性检查、密码存储、消息认证码)(哈希函数、哈希算法)(将任意长度的消息变成固定长度的短消息)

文章目录 数字摘要的概念和应用数字摘要的基本原理数字摘要的性质1. 不可逆性&#xff1a;从数字摘要无法反推出原始输入。这意味着&#xff0c;如果你只知道H(m)&#xff0c;那么你无法得知m的具体内容。&#xff08;除非暴力破解&#xff09;2. 唯一性&#xff1a;对于不同的…

遇到java.security.AccessControlException:access denied怎么办?

今天工作中遇到了如下报错&#xff0c;记录一下解决方案。 目录 问题 分析 结论 问题 这个问题出现在openjdk8启动网页端Java应用。 Java Exception:java.security.AccessControlException:access denied("java.net.SocketPermission""22.188.130.11:9000…

在 M1 芯片 Mac 上使用 Homebrew

在 M1 芯片 Mac 上使用 Homebrew 1.安装brew&#xff08;国内源&#xff09;&#xff1a; /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"2.M1芯片安装完成后会出现&#xff0c;终端输入brew 会出现 zsh: command not f…

浅谈Vue 3的响应式对象: ref和reactive

Vue 3是一个流行的前端框架&#xff0c;它引入了一些新的特性来提高开发者的体验和性能。其中&#xff0c;响应式对象是 Vue 3 中一个非常重要的概念。在这篇博客中&#xff0c;我们将重点介绍 Vue 3 中的响应式对象&#xff0c;并深入探讨其中的 ref 和 reactive。 引言 在现…

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

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

LeetCode算法题解(回溯)|LeetCode93. 复原 IP 地址、LeetCode78. 子集、LeetCode90. 子集 II

一、LeetCode93. 复原 IP 地址 题目链接&#xff1a;93. 复原 IP 地址 题目描述&#xff1a; 有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔。 例如&#xff1a;"0.…