C#事件实例详解
一、什么是事件?
在C#中,事件(event)是一种特殊的类成员,它允许类或对象通知其他类或对象发生了某些事情。
从语法上看,事件的声明类似于字段,但它们在功能和行为上有一些重要的区别。
从技术角度来说,事件实际上是一个封装了事件订阅和取消订阅功能的委托字段。当声明一个事件时,编译器会在背后生成一个私有的委托字段,以及公共的添加(add)和移除(remove)访问器方法。
例如事件声明时:
public event EventHandler<TextArgs> TextPublish;
编译器实际上会生成类似如下的代码:
private EventHandler<TextArgs> _textPublish;public event EventHandler<TextArgs> TextPublish{add { _textPublish += value; }remove { _textPublish -= value; }}
这里,_textPublish是一个私有的委托字段,它存储了所有订阅了TextPublish事件的事件处理方法。add和remove访问器方法提供了订阅和取消订阅事件的功能,它们分别对应了+=和-=操作符。
所以,从这个角度来看,事件更像是一个特殊的属性(property),它封装了一个私有的委托字段,并提供了特定的访问方法。
但是,与普通的属性不同,事件有一些特殊的限制:
1. 事件只能在声明它的类内部直接调用。在类外部,只能通过+=和-=操作符来订阅和取消订阅事件,不能直接读取或赋值事件。
2. 事件通常应该是公共的(public),以允许其他类订阅和取消订阅。但是,事件的访问器方法(add和remove)通常应该是私有的或受保护的,以防止在类外部直接调用。
3. 事件通常应该在引发事件的类中声明,而事件处理方法通常应该在订阅事件的类中定义。这体现了事件的发布-订阅模型。
所以,事件是一种特殊的类成员,它结合了字段、属性和方法的某些特性,用于实现事件驱动编程。理解事件的特殊性质,对于正确地使用和管理事件非常重要。
二、实例说明
举例:在Form1与Form2中有button1和textbox1。
点击Form1中的button1显示Form2。点击Form2中的button1,将Form2中textbox1中的内容发送到Form1中的textbox1中去。
设计,用事件来处理。在Form2声明事件变量,实行触发与发布。在Form2中进行事件处理与事件订阅。
form2中:
public partial class Form2 : Form{public event EventHandler<TextArgs> TextPublish;//apublic Form2()//b{InitializeComponent();}private void button1_Click(object sender, EventArgs e)//c{TextPublish?.Invoke(this, new TextArgs(textBox1.Text));//d}}public class TextArgs : EventArgs//e{public string Text;//fpublic TextArgs(string msg)//h{Text = msg;}}
form1中:
public partial class Form1 : Form{public Form1(){InitializeComponent();}private void button1_Click(object sender, EventArgs e){Form2 fr2 = new Form2();//ifr2.TextPublish += ShowFromForm2;//jfr2.Show();//k}public void ShowFromForm2(object o, TextArgs e)//m{textBox1.Text = e.Text;//n}}
(1)问:a处是定义事件变量,如何理解?前后没看到它的实例化,它在哪儿?
答:event关键字表示定义一个事件,EventHandler<TextArgs>是事件的委托类型,其中TextArgs是自定义的事件参数类。EventHandler<T>中第一个参数固定为object,表示事件的发送者,第二个参数为T类型,是事件的参数。TextPublish是这个事件的名字。事件的定义必须在类内部,因为它是类的成员。
public event EventHandler<TextArgs> TextPublish;这行代码只是声明了一个事件,但并没有实例化。它类似于string a;,只是声明了一个字段,但没有给它赋值。
在C#中,事件的声明类似于字段,但又有一些不同。事件声明后,编译器会自动生成一个私有的委托字段来存储事件的处理程序,并生成add和remove访问器来添加或移除事件处理程序。所以我们可以直接使用+=和-=操作符来订阅和取消订阅事件。
在d处,TextPublish.Invoke(...)能够成功调用,是因为在调用之前,这个事件已经被其他地方的代码订阅了(j处),也就是说有地方使用了TextPublish += ...;的语句。如果事件没有被任何地方订阅,那么Invoke调用时,TextPublish的值实际上是null,会抛出NullReferenceException异常。
不必显式地实例化事件。
当其他地方(如Form1)订阅了TextPublish事件时,C#编译器会自动实例化TextPublish事件的委托字段。这个委托字段最初为null,当第一次使用+=操作符添加事件处理程序时,编译器会创建一个新的委托实例,并将其赋值给这个字段。此后,这个字段就不再为null了。每次使用+=添加新的事件处理程序,都会创建一个新的委托实例,并将其与现有的委托实例合并。
这个自动实例化的过程是由C#编译器在背后完成的,不需要手动去实例化事件。这也是为什么可以直接使用+=和-=操作符来管理事件订阅的原因。这种设计大大简化了事件的使用,让我们可以专注于事件的订阅和发布,而不需要关心事件的实例化细节。
(2)问:b处警告退出时textpublish必须非空?是什么意思,会有什么后果,如何避免?
答:警告说明:在Form2的构造函数退出时,TextPublish事件必须被赋值,不能为null。如果TextPublish为null,在调用事件时会导致NullReferenceException。为避免这个问题,你可以在构造函数中为TextPublish赋一个空的事件处理程序,或者在调用前判断是否为null。
(3)问:c处单击方法里面一般不直接发布?
答:通常在UI事件的处理方法中,如button1_Click,我们一般不直接调用事件的发布方法。而是先进行一些必要的检查和数据准备,然后再发布事件。这是一种良好的编程实践。
(4)问:d处的this是引用的form2?而不是c中参数object的引发者?
答:d处的this确实是指代的当前Form2的实例,而不是Click事件的sender参数。因为我们是在Form2内部发布自己的TextPublish事件。
另外d处并不是最佳写法。应改为:
private void button1_Click(object sender, EventArgs e){string text = textBox1.Text;if (!string.IsNullOrWhiteSpace(text)){OnTextPublish(new TextArgs(text));}}protected virtual void OnTextPublish(TextArgs e){TextPublish?.Invoke(this, e);}
将触发和发布分开写可以提高代码的灵活性和可维护性,使代码更易于理解、扩展和修改。例如:
在发布事件前,可以进行必要的数据检查和准备工作,如上面的代码检查了文本是否为空。
发布事件的逻辑被封装在一个单独的受保护的虚方法OnTextPublish中,子类可以重写这个方法来添加或修改发布事件的逻辑。
在OnTextPublish中,使用了null条件运算符?.来检查事件是否为null,避免了可能的NullReferenceException。事件发布的方法还可以提供一些额外的处理逻辑,例如日志记录、异常处理等。在OnTextPublish方法中,可以针对特定的事件进行一些前置或后置处理,以增加代码的健壮性和可靠性。
这种模式在.NET的类库中广泛使用,它提供了更好的灵活性、可扩展性和安全性。
(5)问:e一般继承事件参数,是按照单击事件后面跟e参数一样?f处就跟具体的信息,由h处构造函数传送过来?
答:对,自定义事件的参数类一般继承自EventArgs,并添加需要传递的数据作为公共属性,就像TextArgs.Text一样。这些数据在构造函数中初始化,在事件处理程序中使用。
EventHandler<TextArgs>说明EventHandler<T>预定义的第二个参数必须是EventArgs的派生类,如果自定义的事件参数类不继承EventArgs(e处),就会产生类型不兼容的错误。所以按照约定,自定义事件参数类都应该直接或间接继承自EventArgs。
也可以不使用继承EventArgs的类型,比如EventHandler<string>。然后直接传递一个字符串作为事件参数。这在语法上是允许的。但是,这并不是一个好的实践。
使用EventArgs派生类有三处好处,表示这个类是事件参数类、传递多个参数时直接融入多个属性即可、方便自己和他人阅读和维护。所以,即使事件参数只有一个字符串,也建议定义一个继承自EventArgs的类来包装它。
(6)问:h处警告:使用主构造函数,这是什么意思?只听说过构造函数,没听说过主构造函数?
答:"使用主构造函数"警告可能是因为你使用了C# 9.0或更高版本引入的"主构造函数"特性。主构造函数可以简化类的定义,将构造函数参数直接放在类名后面,省去了单独定义字段的麻烦。不过主构造函数只能有一个,如果定义了主构造函数,就不能再有其他构造函数了。这个警告提示你考虑使用主构造函数语法。
主构造函数允许你在类名后面直接添加构造函数参数,编译器会自动生成对应的私有字段和构造函数。例如:
public class Person{public Person(string firstName, string lastName){FirstName = firstName;LastName = lastName;}public string FirstName { get; }public string LastName { get; }}
可以简写为:
public class Person(string FirstName, string LastName){}
主构造函数可以简化类的定义,减少重复代码。
但它也有一些限制,例如只能有一个主构造函数,如果定义了主构造函数就不能再定义其他构造函数等。
传统的构造函数语法更灵活,可以定义多个重载的构造函数,也可以在构造函数中编写更复杂的初始化逻辑。
主构造函数更适合那些简单的、只需要初始化几个属性的类。
对于有默认值的主构造函数还可以这样:
public class Person(string FirstName = "John", string LastName = "Doe"){}
在主构造函数中为参数提供默认值,相当于为属性提供了默认值。如果在创建类的实例时没有提供参数,那么这些属性就会使用默认值。
public class Person(string FirstName = "John", string LastName = "Doe"){}// 使用默认值Person p1 = new Person();Console.WriteLine($"{p1.FirstName} {p1.LastName}"); // 输出: John Doe// 提供参数Person p2 = new Person("Jane", "Smith");Console.WriteLine($"{p2.FirstName} {p2.LastName}"); // 输出: Jane Smith
(7)问:m处事件的处理,该方法需要与a处签名一致?
答:事件处理方法的签名必须与事件的委托类型匹配。在这个例子中,TextPublish事件的类型是EventHandler<TextArgs>,所以事件处理方法ShowFromForm2必须有一个object类型的参数(事件发送者)和一个TextArgs类型的参数(事件参数)。在方法体内,我们可以直接使用事件参数e的属性,如e.Text。
(8)问:i处实例化时就自动有了TextPubublish成员,通过+=实现了form2中事件变量的变例化,原为null的textpublish得到了showformform2因此不空了。
答:当Form2的实例fr2被创建时,它的TextPublish事件字段初始为null。但在下一行代码fr2.TextPublish += ShowFromForm2;中,通过+=操作符,将ShowFromForm2方法订阅到了TextPublish事件。这个操作会导致TextPublish字段被实例化为一个新的委托对象,并将ShowFromForm2方法添加到这个委托对象中。因此,TextPublish不再为null。
(9)问:k处用了show。如果用了showdialog()将是模态输出,可能还有一个隐患?
答:Show方法会以非模态的方式显示Form2,这意味着Form1还可以继续响应用户的交互。而ShowDialog会以模态的方式显示Form2,这意味着在Form2关闭之前,用户不能与Form1交互,因此ShowDialog后面的语句可以无法得到Form1的响应。例如:
Form2 fr2 = new Form2();//i
fr2.ShowDialog();//k
fr2.TextPublish += ShowFromForm2;//j
当k语句执行后,因为模态的原因,form1将“冻结”无法响应,也就无法继续执行j句,因此,后面点击后事件实际上没有发生,因为j句没有执行,那么事件就没有实例化是一个null,在d处是一个空合并操作,为null不会执行后面操作,所以没有反应。如果改为TextPublish.Invoke(this, new TextArgs(textBox1.Text));在点击后因为TextPublish为空将出现异常。所以这里最好用show,若用showdialog则放在最后。
相关文章:

C#事件实例详解
一、什么是事件? 在C#中,事件(event)是一种特殊的类成员,它允许类或对象通知其他类或对象发生了某些事情。 从语法上看,事件的声明类似于字段,但它们在功能和行为上有一些重要的区别。 从技术角度来说,事件实际上是一个封装了事件订阅和取消订阅功能的委托字段。…...

零基础机器学习(3)之机器学习的一般过程
文章目录 一、机器学习一般过程1.数据获取2.特征提取3.数据预处理①去除唯一属性②缺失值处理A. 均值插补法B. 同类均值插补法 ③重复值处理④异常值⑤数据定量化 4.数据标准化①min-max标准化(归一化)②z-score标准化(规范化) 5.…...

用java做一个双色球彩票系统
代码如下: import java.util.Random; public class HelloWorld{public static void main(String[] args){//1、生成中奖号码 int[] arrcreateNumber();for (int i 0;i<arr.length;i) {System.out.print(arr[i]" ");}}public static int[] createNu…...

某对象存储元数据集群改造流水账
软件产品:某厂商提供的不便具名的对象存储产品,核心底层技术源自HDFS和Amazon S3,元数据集群采用了基于MongoDB的NOSQL数据库产品和MySQL数据库产品相结合。 该产品的元数据逻辑示意图如下: 业务集群现状:当前第3期建…...

前端理论总结(js)——filter、foearch、for in 、for of 、for的区别以及返回值
Filter: 用途:用于筛选数组中符合条件的元素,返回一个新数组。 返回值:返回一个新数组,包含经过筛选的元素。 Foreach: 用途:遍历数组中的每个元素,执行回调函数。 返回值&#x…...

【JavaEE初阶系列】——多线程案例一——单例模式 (“饿汉模式“和“懒汉模式“以及解决线程安全问题)
目录 🚩单例模式 🎈饿汉模式 🎈懒汉模式 ❗线程安全问题 📝加锁 📝执行效率提高 📝指令重排序 🍭总结 单例模式,非常经典的设计模式,也是一个重要的学科&#x…...

革新水库大坝监测:传统软件与云平台之比较
在水库大坝的监测管理领域,传统监测软件虽然曾发挥了重要作用,但在多方面显示出了其局限性。传统解决方案通常伴随着高昂的运维成本,需要大量的硬件支持和人员维护,且软件整合和升级困难,限制了其灵活性和扩展性。 点击…...

C++模版(基础)
目录 C泛型编程思想 C模版 模版介绍 模版使用 函数模版 函数模版基础语法 函数模版原理 函数模版实例化 模版参数匹配规则 类模版 类模版基础语法 C泛型编程思想 泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。 模板是泛型编程…...

MySQL驱动Add Batch优化实现
MySQL 驱动 Add Batch 优化实现 MySQL 驱动会在 JDBC URL 添加 rewriteBatchedStatements 参数时,对 batch 操作进行优化。本文测试各种参数组合的行为,并结合驱动代码简单分析。 batch参数组合行为 useServerPrepStmts 参数 PreparedStatement psmt…...

手撕算法-数组中的第K个最大元素
描述 分析 使用小根堆,堆元素控制在k个,遍历数组构建堆,最后堆顶就是第K个最大的元素。 代码 class Solution {public int findKthLargest(int[] nums, int k) {// 小根堆PriorityQueue<Integer> queue new PriorityQueue<>…...

【vue】computed和watch的区别和应用场景
Computed 和 Watch 是 Vue.js 中用于监视数据变化的两个不同特性,它们各自有不同的应用场景和功能。 Computed: 计算属性(Computed properties)用于声明基于其他数据属性的计算值。它具有缓存功能,只有在依赖的数…...

ARM.day8
1.自己设置温度湿度阈值,当温度过高时,打开风扇,蜂鸣器报警 2.当湿度比较高时,打开LED1灯,蜂鸣器报警 main.c #include "si7006.h" #include "CH1.h" #include "led.h" // 延时函数in…...

SpringCloud Gateway工作流程
Spring Cloud Gateway的工作流程 具体的流程: 用户发送请求到网关 请求断言,用户请求到达网关后,由Gateway Handler Mapping(网关处理器映射)进行Predicates(断言),看一下哪一个符合…...

西井科技与安通控股签署战略合作协议 共创大物流全新生态
2024年3月21日,西井科技与安通控股在“上海硅巷”新象限空间正式签署战略合作框架协议。双方基于此前在集装箱物流的成功实践与资源优势,积极拓展在AI数字化产品、新能源自动驾驶解决方案和多场景应用,以及绿色物流链等领域的深度探索、强强联…...

CCCorelib 点云RANSAC拟合球体(CloudCompare内置算法库)
文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 RANSAC是一种随机参数估计算法。RANSAC从样本中随机抽选出一个样本子集,使用最小方差估计算法对这个子集计算模型参数,然后计算所有样本与该模型的偏差,再使用一个预先设定好的阈值与偏差比较,当偏差小于阈值时…...

map china not exists. the geojson of the map must be provided.
map china not exists. the geojson of the map must be provided. 场景:引入echarts地图报错map china not exists. the geojson of the map must be provided. 原因: echarts版本过高,ECharts 之前提供下载的矢量地图数据来自第三方&…...

Redis如何删除大key
参考阿里云Redis规范 查找大key: redis-cli --bigkeys 1、String类型: Redis 4.0及以后版本提供了UNLINK命令,该命令与DEL命令类似,但它会在后台异步删除key,不会阻塞当前客户端,也不会阻塞Redis服务器的…...

JRT菜单
上一章搭建了登录界面的雏形和抽取了登录接口。给多组使用登录和菜单功能提供预留,做到不强行入侵别人业务。任何产品只需要按自己表实现登录接口后配置到容器即可共用登录界面和菜单部分。最后自己的用户关联到JRT角色表即可。 登录效果 这次构建菜单体系 首先用…...

《海王2》观后感
前言 我原本计划电影上映之后,去电影院观看的,但时间过得飞快,一眨眼这都快4月份了,查了一下,电影院早就没有排片了,所以只能在B站看了,这里不得不吐槽一下,原来花了4块钱购买观看还…...

[蓝桥杯 2023 省 A] 颜色平衡树:从零开始理解树上莫队 一颗颜色平衡树引发的惨案
十四是一名生物工程的学生,他已经7年没碰过信息学竞赛了,有一天他走在蓝桥上看见了一颗漂亮的颜色平衡树: [蓝桥杯 2023 省 A] 颜色平衡树 - 洛谷 十四想用暴力解决问题,他想枚举每个节点,每个节点代表…...

maya打开bvh脚本
目录 maya打开脚本编辑器 运行打开bvh脚本 maya导出bvh脚本 maya打开脚本编辑器 打开Maya软件,点击右下角 “脚本编辑器” 运行打开bvh脚本 https://github.com/jhoolmans/mayaImporterBVH/blob/master/bvh_importer.py import os import re from typing impo…...

【JavaSE】数据类型和运算符
前言 从这一篇我们开始Java的学习~ 欢迎关注个人主页:逸狼 创造不易,可以点点赞吗~ 如有错误,欢迎指出~ 目录 前言 Java第一个程序 字面常量 字面常量的分类 结合代码理解 类型转换 类型提升 byte与byte的运算 正确写法 字符串类型St…...

Docker 哲学 - ip 的组成规则 与 网关介绍
在 IP 地址中,我们通常将 IP 地址分为两部分:网络部分和主机部分。网络部分用于标识网络,主机部分用于标识该网络中的特定主机。 IP 地址的每个部分(也被称为一个八位组或一个字节)可以是从0到255的任何值。 一个 IPv4…...

数学建模竞赛真的是模型解题一般,但是论文出彩而获奖的吗?
最近,数乐君发现有同学会有这样的问题:在数学建模国赛中,会因为参赛团队的模型解题一般,但论文写得非常精彩而获奖吗? 是的,确实会存在这样的情况。 我们都知道数学建模竞赛最终都是以提交成品论文的形式…...

深度学习常见的三种模型
深度学习模型实际上是一个包含多个隐藏层的神经网络,目前主要有卷积神经网络(CNN)、深度置信网络(DBN)、循环神经网络(RNN)。 1) 卷积神经网络 在机器学习领域,卷积神经网络属于前…...

接口自动化测试分层设计与实践总结
🍅 视频学习:文末有免费的配套视频可观看 🍅 关注公众号:互联网杂货铺,回复1 ,免费获取软件测试全套资料,资料在手,涨薪更快 接口测试三要素: 参数构造 发起请求&#x…...

集合(下)Map集合的使用
文章目录 前言一、Map接口二、Map接口的实现类 1.HashMap类2.TreeMap类总结 前言 Map集合没有继承Collection接口,不能像List集合和Set集合那样直接使用Collection接口的方法。Map集合其自身通过以key到value的映射关系实现的集合,也有相应的许多方法。类…...

AAPT: error: resource android:attr/dialogCornerRadius not found.
ERROR:D:\android.gradle\caches\transforms-3\b3b98118f65da38d0ad9da84cfc70a72\transformed\appcompat-1.0.0\res\values-v28\values-v28.xml:5:5-8:13: AAPT: error: resource android:attr/dialogCornerRadius not found. 请帮我看看这个错误是什么意思。我改如何做。 这个…...

数字功放VS模拟功放,选择适合你的音频解决方案
数字功放和模拟功放是音频系统中常用的两种功放技术,适用于不同的音频应用,都具有各自的优势和特点。本文将为您详细介绍数字功放和模拟功放的差异,并帮助您找到适合自己的音频解决方案。 1、数字功放是一种利用数字信号处理技术的功放。它将…...

5.88 BCC工具之tcpsynbl.py解读
一,工具简介 tcpsynbl工具以直方图的形式显示SYN到达时的TCP SYN积压大小。这可以让我们了解应用程序距离达到积压限制并丢弃SYN(导致SYN重传产生性能问题)还有多远。 TCP SYN 数据包则通常用于启动 TCP 三次握手过程的第一次握手。 二,代码示例 #!/usr/bin/env python…...