【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:64
		customer.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出来的委托字段,一般而言,我们不希望通过这样的方式去为其他类触发事件。这会造成一些逻辑上的错误。使用事件,就可以把其对应的委托封装起来,避免一些奇怪的用法。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

介绍两个好用又好玩的大模型工具

先让数字人跟大家打个招呼吧。 我的AI数字人会手语了 发现没&#xff0c;我的数字人本周又学了一个新技能&#xff1a;手语。 这些数字人都是通过AI生成的。 但数字人不是今天的主题&#xff0c;今天要跟大家聊聊大模型。 自从大模型出现后&#xff0c;很多人&#xff08;包…

20行JS代码实现屏幕录制

在开发中可能有遇到过屏幕录制的需求&#xff0c;无论是教学、演示还是游戏录制&#xff0c;都需要通过屏幕录制来记录和分享内容。一般在App内H5页基于客户端能力实现的较多&#xff0c;现在浏览器中的 MediaRecorder 也提供了这种能力。MediaRecorder 是一种强大的技术&#…

Mybatis(一)

1. Mybatis简介 MyBatis下载地址 1.1 MyBatis历史 MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下&#xff0c;iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github…

【遍历二叉树的非递归算法,二叉树的层次遍历】

文章目录 遍历二叉树的非递归算法二叉树的层次遍历 遍历二叉树的非递归算法 先序遍历序列建立二叉树的二叉链表 中序遍历非递归算法 二叉树中序遍历的非递归算法的关键&#xff1a;在中序遍历过某个结点的整个左子树后&#xff0c;如何找到该结点的根以及右子树。 基本思想&a…

4个杀手级Pycharm高效插件

本文将介绍4个学习Python的人都应该安装的Pycharm插件&#xff0c;通过这些插件提高工作效率并使Pycharm看起来更美观。 1、简介 Pycharm是Python最受欢迎的集成开发环境之一。它具有良好的代码助手、漂亮的主题和快捷方式&#xff0c;使编写代码变得简单快捷。 话虽如此&…

深度学习中的图像增强合集

引言 图像增强是我们在深度学习领域中绕不开的一个话题&#xff0c;本文我们将讨论什么是图像增强&#xff0c;并在三个不同的 python 库中实现它&#xff0c;即 Keras、Pytorch 和 augmentation&#xff08;专门用于图像增强的一个库&#xff09;。所以第一个问题就是什么是图…

Linux shell编程学习笔记21:用select in循环语句打造菜单

一、select in循环语句的功能 Linux shell脚本编程提供了select in语句&#xff0c;这是 Shell 独有的一种循环语句&#xff0c;非常适合终端&#xff08;Terminal&#xff09;这样的交互场景&#xff0c;它可以根据用户的设置显示出带编号的菜单&#xff0c;用户通过输入不同…

nginx-配置拆分(各个模块详细说明)

主配置文件 配置结构 ... #nginx全局块events { #events块... #events块 }http { #http块... #http全局块server { #server块... #server全局块location [PATTERN] { #location块... #location块}location [PATTERN] {...}}serv…

高性能网络编程 - The C10K problem 以及 网络编程技术角度的解决思路

文章目录 C10KC10K的由来C10K问题在技术层面的典型体现C10K问题的本质C10K解决思路思路一&#xff1a;每个进程/线程处理一个连接思路二&#xff1a;每个进程/线程同时处理多个连接&#xff08;IO多路复用&#xff09;● 实现方式1&#xff1a;直接循环处理多个连接● 实现方式…

线上 kafka rebalance 解决

上周末我们服务上线完毕之后发生了一个kafka相关的异常&#xff0c;线上的kafka频繁的rebalance&#xff0c;详细的报错我已经贴到下面&#xff0c;根据字面意思&#xff1a;消费者异常 org.apache.kafka.clients.consumer.CommitFailedException: 无法完成提交&#xff0c;因为…

设计模式-状态模式 golang实现

一 什么是有限状态机 有限状态机&#xff0c;英⽂翻译是 Finite State Machine&#xff0c;缩写为 FSM&#xff0c;简称为状态机。 状态机不是指一台实际机器&#xff0c;而是指一个数学模型。说白了&#xff0c;一般就是指一张状态转换图。 已订单交易为例&#xff1a; 1.…

unity打AB包,AssetBundle预制体与图集(三)

警告&#xff1a; spriteatlasmanager.atlasrequested wasn’t listened to while 条件一&#xff1a;图片打图集里面去了 条件二&#xff1a;然后图集打成AB包了 条件三&#xff1a;UI预制体也打到AB包里面去了 步骤一&#xff1a;先加载了图集 步骤二&#xff1a;再加载UI预…

Spring Cloud LoadBalancer基础知识

LoadBalancer 概念常见的负载均衡策略使用随机选择的负载均衡策略创建随机选择负载均衡器配置 Nacos 权重负载均衡器创建 Nacos 负载均衡器配置 自定义负载均衡器(根据IP哈希策略选择)创建自定义负载均衡器封装自定义负载均衡器配置 缓存 概念 LoadBalancer(负载均衡器)是一种…

jenkins部署job

apt install fontconfig openjdk-11-jre wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/war/2.429/jenkins.wardeb包安装 wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/debian-stable/jenkins_2.414.3_all.debdpkg -i jenkins_2.414.3_all.deb 访问 http://…

垂直领域大模型落地思考

相比能做很多事&#xff0c;但每件事都马马虎虎的通用大模型&#xff1b;只能做一两件事&#xff0c;但这一两件事都能做好&#xff0c;可被信赖的垂直大模型会更有价值。这样的垂直大模型能帮助我们真正解决问题&#xff0c;提高生产效率。 本文将系统介绍如何做一个垂直领域…

JavaScript脚本操作CSS

脚本化CSS就是使用JavaScript脚本操作CSS&#xff0c;配合HTML5、Ajax、jQuery等技术&#xff0c;可以设计出细腻、逼真的页面特效和交互行为&#xff0c;提升用户体验&#xff0c;如网页对象的显示/隐藏、定位、变形、运动等动态样式。 1、CSS脚本化基础 CSS样式有两种形式&…

学习笔记:CANOE模拟LIN主节点和实际从节点进行通信测试

先写点感想&#xff0c;在LIN开发阶段&#xff0c;我一般用图莫斯USB工具来进行模拟主机节点发送数据。后来公司买了CANOE工具就边学习边搭建了LIN的测试工程&#xff0c;网上的资料真的很少&#xff0c;主要是靠自己一点点摸索前进&#xff0c;总算入门。几个月后的今天&#…

基于swing的人事管理系统

概述 个人项目人事管理系统&#xff0c;针对部门和人员之间的管理。 详细 一、项目UI 二、项目结构 三、项目使用方法 Eclipse导入现有现有项目到工作空间即可&#xff0c;会自动加载包内相关jar包&#xff0c;使用的java源文件 四、部分代码 MainFrm.java package view…

Docker指定容器使用内存

Docker指定容器使用内存 作者&#xff1a;铁乐与猫 如果是还没有生成的容器&#xff0c;你可以从指定镜像生成容器时特意加上 run -m 256m 或 --memory-swap512m来限制。 -m操作指定的是物理内存&#xff0c;还有虚拟交换分区默认也会生成同样的大小&#xff0c;而–memory-…

ubuntu系统黑屏,且光标不闪烁

选择第二个&#xff0c;进入恢复模式 选择第二个&#xff0c;进入恢复模式 选择root 输入&#xff1a; startx然后就可以进入文本界面或者图形化界面了&#xff0c;如果不行&#xff0c;报错&#xff0c;可能需要需要下载这个包&#xff0c;把这个错误到网上搜索一下就可以找…