在C#编程语言中,事件(Event)是一个非常重要的概念,它提供了一种松耦合的方式,让对象间能够通知彼此,而无需直接联系。事件的使用可以让我们的代码更加灵活、可扩展且易于维护。

        事件可以视作委托的实例,因此熟悉委托的使用对理解事件是有帮助的(如果学过C语言,委托就相当于定义一个指向函数的指针类型,事件就相当于指向函数的指针变量);不熟悉委托也不影响本文的阅读。

        本文将通过一个经典的生活案例“闹钟一响,打工人就要起床了”来讲解C#中事件(Event)的基本概念和使用方法。

一、事件的基本概念

        通过事件,可以让一个类(发布者c1)向另一个类(订阅者c2)发出通知,告诉它某个特定操作已经发生,简单理解就是在c1中调用c2中的方法。

        那事件的核心问题是:如何在c1调用c2中的方法f2。

        因用委托可以实现:在c1调用c2中的方法f2。 所以这里先明白委托的使用,明白了委托的使用,弄明白事件就是水到渠成的事。

       委托有四要素:

委托类型的定义    ——q1(输入参数类型、输入参数个数、返回值类型)
委托变量的定义    ——q2
委托变量的实例化  ——q3
委托变量的调用    ——q4

二、经典案例:“闹钟响了,打工人就要起床了”

用“闹钟响了,打工人就要起床了”作为一个现实中的例子,来模拟C#中的事件。假设我们有一个闹钟(发布者c1)和一个打工人(订阅者c2)。每当闹钟响起时,打工人就要起床上班了。这个过程中的关键是,打工人并不直接控制闹钟,而是闹钟响起这个动作触发了打工人的起床动作--闹钟一响打工人就起床,这不就是在闹钟响的方法里调用了打工人的起床方法吗。

委托类型的定义    ——q1    ——起床动作的类型
委托变量的定义    ——q2    ——指向起床动作的变量
委托变量的实例化  ——q3    ——指向起床动作变量的赋值
委托变量的调用    ——q4    ——指向起床动作变量的调用

三、用C#模拟“闹钟响了,打工人就要起床了”

下面是实现这个经典案例C#代码:

using System;

namespace Demo
{
	class Program
	{
		static void Main(string[] args)
		{
			Alarms alarm = new Alarms();
			Workers worker = new Workers();

			alarm.OnRingSoundEvent = worker.getupWorker;//委托变量的实例化  ——q3    ——指向起床动作变量的赋值

			Console.Write("请输入闹钟参数(表示闹钟响的次数):");
			uint nClock = Convert.ToUInt16(Console.ReadLine());
			alarm.ringAlarm(nClock);

			Console.ReadLine();
		}
	}

	public class Alarms                                 //2个类之c1:发布者→ 闹钟	
	{
		public delegate void RingHandler();             //委托类型的定义    ——q1    ——起床动作的类型→ 起床动作的类型:输入参数类型、输入参数个数、返回值类型
		public RingHandler OnRingSoundEvent;            //委托变量的定义    ——q2    ——指向起床动作的变量

		public void ringAlarm(uint ringKind)            //闹钟的响铃动作
		{
			Console.WriteLine($"闹钟响了{ringKind}次。");
			OnRingSoundEvent();                         //委托变量的调用    ——q4    ——指向起床动作变量的调用
														//这是保证铃声一响,打工人就起床的关键
		}
	}

	public class Workers                                
	{
		public void getupWorker()                      //打工人的起床动作
		{
			Console.WriteLine($"打工人起床了!");
		}
	}
}

程序运行结果 

请输入闹钟参数(表示闹钟响的次数):2
闹钟响了2次。
打工人起床了!

 四、为什么使用事件?

        通过上面这个例子发现使用委托已经能够实现c1中调用c2中的方法,那为什么还要引入事件呢?因为在对委托进行的实例化时,直接使用=符号,可能赋值一个空引用,这些操作会对安全性造成威胁。

        为了解决这个问题,C#提供了专门的事件处理机制,以保证事件订阅的可靠性。事件通过在委托变量的定义中添加event关键字来实现,如下所示:

public event RingHandler OnRingSoundEvent;

此时,以下代码会出现编译错误:

alarm.OnRingSoundEvent = worker.getupWorker;  // 错误:不能直接赋值

alarm.OnRingSoundEvent = null;                // 错误:不能直接赋值为null

而通过订阅事件时,可以使用+=来注册事件处理方法:

alarm.OnRingSoundEvent += worker.getupWorker;     // 正确:订阅事件

alarm.OnRingSoundEvent -= worker.getupWorker;     // 正确:取消订阅事件

下面是“event”正统代码

using System;

namespace Demo
{
	class Program
	{
		static void Main(string[] args)
		{
			Alarms alarm = new Alarms();
			Workers worker = new Workers();
			worker.subscribeToRing(alarm);              //类作为实参进行传递时是地址传递

			Console.Write("请输入闹钟参数(表示闹钟响的次数):");
			uint nClock = Convert.ToUInt16(Console.ReadLine());
			alarm.ringAlarm(nClock);

			Console.ReadLine();
		}
	}

	public class Alarms                                 //2个类之c1:发布者→ 闹钟	
	{
		public delegate void RingHandler();             //委托类型的定义    ——q1    ——起床动作的类型→ 起床动作的类型:输入参数类型、输入参数个数、返回值类型
		public RingHandler OnRingSoundEvent;            //委托变量的定义    ——q2    ——指向起床动作的变量

		public void ringAlarm(uint ringKind)            //闹钟的响铃动作
		{
			Console.WriteLine($"闹钟响了{ringKind}次。");
			OnRingSoundEvent();                         //委托变量的调用    ——q4    ——指向起床动作变量的调用
														//这是保证铃声一响,打工人就起床的关键
		}
	}

	public class Workers                                
	{
		public void subscribeToRing(Alarms a)
		{
			a.OnRingSoundEvent += getupWorker;               //委托变量的实例化  ——q3    ——指向起床动作变量的赋值
		}
		public void getupWorker()                      //打工人的起床动作
		{
			Console.WriteLine($"打工人起床了!");
		}
	}
}

五、总结

        通过这个经典案例“闹钟响了,打工人就要起床了”,我们可以看到事件如何使得C#程序中的各个部分保持松耦合,发布者与订阅者之间并不直接依赖,而是通过事件机制进行通信。这种设计使得我们的代码更加灵活和可扩展。

        后续的系统性学习可以看【C#从入门到精通(第6版)】的《17.5 事件》或【叩响C#之门】的《16.4 事件处理机制》。

参考

清华大学出版社-图书详情-《C#从入门到精通(第6版)》

叩响C#之门 (豆瓣)

C# 事件(Event) | 菜鸟教程

C#事件--全网最全+全网最易理解-CSDN博客

ChatGPT | OpenAI

更多推荐