c#中“事件-event”的经典示例与理解
在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 事件处理机制》。
参考
更多推荐



所有评论(0)