【C#学习笔记】事件

news2024/11/24 8:39:39

在这里插入图片描述


前言

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

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

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

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


文章目录

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


事件的定义

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

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

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

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

相关文章

Android 深色模式切换适配

在Android11上测试 1&#xff0c;把需要适配的资源文件复制一份后缀加上-night&#xff0c;里面就放置变主题后的资源 2&#xff0c;两个主题一个白&#xff0c;一个黑&#xff0c;分别放置在对应的valuse-styles.xml中 <style name"Theme.LaserMachPor" parent&…

MATLAB / Simulink HDL 快速入门

MATLAB / Simulink HDL 快速入门 我们将使用实例讲解MATLAB / Simulink HDL 使用入门。 开始这个项目&#xff0c;首先需要创建一个包含 Stateflow 的新 Simulink 。只需单击画布中的任意位置并开始输入 Stateflow。 此时应该能在画布上看到 Stateflow 图标。双击图标进行编辑。…

埃安AION V Plus 80星辰版:纯电家庭SUV市场的新续航里程碑

埃安汽车全新AION V Plus 80星辰版上市&#xff1a;定位600km续航家庭SUV&#xff0c;亲民价引领市场新趋势 埃安汽车宣布其全新车型——AION V Plus 80星辰版正式上市&#xff0c;以18.59万元的竞争性价格和超过600公里的续航力&#xff0c;响应了中国家庭对于多功能、…

微软Azure OpenAI申请和使用教程

新版已增加微软Azure OpenAI接口,申请教程对照如下 一、申请使用Azure OpenAI服务 二、配置Azure OpenAI 打开这个页面:https://portal.azure.com/?quickstart=true#create/Microsoft.CognitiveServicesOpenAI 进入 Azure 后,直接搜索OpenAI 若是已经通过,这里即可选择订阅…

C语言实现输入 n 个字符串,将它们按字母由小到大的顺序排列并输出

完整代码&#xff1a; // 输入 n 个字符串&#xff0c;将它们按字母由小到大的顺序排列并输出 #include<stdio.h> #include<stdlib.h> //字符串的最大长度 #define N 20//交换两个字符串在数组中的位置 void swap(char **str1,char **str2){char *temp*str1;*str1…

【数字图像处理-TUST】实验二-图像噪声生成与滤波降噪

一&#xff0c;题目 读入一幅图像使用两种以上的方法向图像中分别添加噪声输出一幅二值图像&#xff0c;背景为黑色&#xff0c;噪声区域为白色使用三种滤波方法对上述添加了噪声的图像进行降噪处理输出降噪处理后的结果图像 二&#xff0c;实验原理 采用了两种方法添加了噪…

7.现代卷积神经网络3-GPT版

#pic_center R 1 R_1 R1​ R 2 R^2 R2 目录 知识框架No.1 深度卷积神经网络 AlexNet一、AlexNet1、AlexNet2、机器学习3、几何学4、特征工程5、HardWare6、数据集7、AlexNet的改进的地方8、AlexNet架构-卷积池化9、AlexNet架构-卷积池化10、AlexNet架构-全连接层11、更多改变细…

Oracle 三种分页方法(rownum、offset和fetch、row_number() over())

Oracle的三种分页指的是在进行分页查询时&#xff0c;使用三种不同的方式来实现分页效果&#xff0c;分别是使用rownum、使用offset和fetch、使用row_number() over() 1、使用rownum rownum是oracle中一个伪劣&#xff0c;它用于表示返回的行的序号。使用rownum进行分页查询的方…

华为交换机忘记console密码怎么办?

console线RJ45头 连接交换机console口&#xff0c;usb接口连接电脑电脑桌面计算机右键-》管理&#xff0c;端口查看端口是com几 3打开secureCRT 点击第二个图标&#xff0c;快速连接&#xff0c;然后设置下参数&#xff0c;如下图 4、重启交换机 5、看到如下图提示信息&#x…

Window下安装 Mongodb,并实现单点事务

在window操作系统下安装Mongodb&#xff0c;并让单点mongodb支持事务&#xff0c;mongodb5以上时才支持事务&#xff0c;所以必须时mongodb5及以上版本才支持。 1、下载mongodb安装文件 &#xff08;1&#xff09; 下载mongodb msi 安装文件 地址&#xff1a;mongocommunity &…

研发项目管理改进方法有哪些

研发项目管理改进方法有哪些 1.多项目协同管理 有可视化的项目进度管理环境&#xff0c;可通过表格视图或施工进度表&#xff0c;项目成员可以共同进行实时项目计划的编制。可以修改项目的任务约束、重大事件以及开始结束日期。还可进行任务分解、任务约束、不限层任务树、任务…

秒懂!用这10款思维导图软件,让头脑风暴如虎添翼!

世界上最糟糕的感觉之一就是忘记了一个伟大的点子。原本你只需把它记下来&#xff0c;但你当时确信自己绝不会忘记如此引人入胜的事物。然而&#xff0c;当这个想法从你的脑海彻底消失时&#xff0c;分分钟会让人崩溃。 如果你的想法有很多组成部分&#xff0c;比如一个大项目…

ChatGPT已经不知不觉改变了我的生活

文章目录 前言GPT技术的广泛应用内容创作智能助手教育和知识分享 机遇与挑战机遇挑战 总结 前言 近年来&#xff0c;自然语言处理技术的巨大进步已经催生了一系列GPT&#xff08;Generative Pre-trained Transformer&#xff09;模型&#xff0c;如ChatGPT、文心一言、C知道等…

AI生图王者之战!深度体验实测,谁是真正的艺术家?

10月11日凌晨&#xff0c;设计软件巨头Adobe宣布推出一系列图像生成模型&#xff0c;其中Firefly Image 2作为新一代图像生成器&#xff0c;通过改善皮肤、头发、眼睛、手和身体结构增强了人体渲染质量&#xff0c;提供更好的色彩和改进的动态范围&#xff0c;并为用户提供更大…

C# Onnx DirectMHP 全范围角度2D多人头部姿势估计

效果 项目 代码 using Microsoft.ML.OnnxRuntime.Tensors; using Microsoft.ML.OnnxRuntime; using OpenCvSharp; using System; using System.Collections.Generic; using System.Windows.Forms; using System.Linq; using System.Numerics;namespace Onnx_Demo {public part…

Leetcode-876 链表的中间结点

本人解法有点硬凑答案… /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; …

京东数据分析:2023年10月京东洗衣机行业品牌销售排行榜

鲸参谋监测的京东平台10月份洗衣机市场销售数据已出炉&#xff01; 10月份&#xff0c;洗衣机市场整体销售呈上升走势。鲸参谋数据显示&#xff0c;今年10月&#xff0c;京东平台洗衣机市场的销量为143万&#xff0c;环比增长约23%&#xff0c;同比增长约1%&#xff1b;销售额约…

千兆工业交换机——工业环境的高速以太网交换机

千兆工业交换机&#xff08;Gigabit Industrial Switch&#xff09;是一种用于工业环境的高速以太网交换机&#xff0c;具有以下特性&#xff1a; 1. 高速传输&#xff1a;支持千兆以太网速率&#xff08;1000Mbps&#xff09;&#xff0c;提供更快的数据传输速度和高带宽。 2.…

C#中.NET 7.0控制台应用使用LINQtoSQL、LINQtoXML

目录 一、新建控制台应用和数据库连接 二、手动添加System.Data.Linq程序包 三、手动添加System.Data.SqlClient程序包 四、再次操作DataClasses1.dbml 五、示例 1.源码 2.xml文件 默认安装的.NET 7.0控制台应用是不支持使用LINQtoSQL、LINQtoXML的。 默认安装的.NET F…