c#中“事件-event”的经典示例与理解
在C#编程语言中,事件(Event)是一个非常重要的概念,它提供了一种松耦合的方式,让对象间能够通知彼此,而无需直接联系。事件的使用可以让我们的代码更加灵活、可扩展且易于维护。
事件可以视作委托的实例,因此熟悉委托的使用对理解事件是有帮助的(如果学过C语言,委托就相当于定义一个指向函数的指针类型,事件就相当于指向函数的指针变量);不熟悉委托也不影响本文的阅读。
本文将通过一个经典的生活案例“闹钟一响,打工人就要起床了”来讲解C#中事件(Event)的基本概念和使用方法。
一、事件的基本概念
通过事件,可以让一个类(发布者c1)向另一个类(订阅者c2)发出通知,告诉它某个特定操作已经发生。
事件的核心问题是:如何用发布者c1中的方法f1去调用订阅者c2中的方法f2。
基本的思路是:在c1中定义一个指向f2方法的变量v,然后在f1中调用v。这样就实现了通过c1中的方法f1来调用c2中的方法f2。
我给概括为以下几个要素:
2个类:
发布者、发送者 → c1
订阅者、接收者 → c2
2个方法:
触发事件的方法 → f1(发布者)
事件的处理方法 → f2(订阅者)
1个变量:
事件 → v(在c1中定义,但不在c1中赋值)
4个关键:
事件的类型 → q1
事件的赋值 → q2
事件的触发 → f2的调用 → q3
触发的调用 → f1的调用 → q4
二、经典案例:“闹钟响了,打工人就要起床了”
用“闹钟响了,打工人就要起床了”作为一个现实中的例子,来模拟C#中的事件。假设我们有一个闹钟(发布者)和一个打工人(订阅者)。每当闹钟响起时,打工人就要起床上班了。这个过程中的关键是,打工人并不直接控制闹钟,而是通过事件来接收到闹钟响起的通知,从而作出反应。
三、用C#模拟“闹钟响了,打工人就要起床了”
下面是实现这个经典案例C#代码:
using System;
namespace Demo
{
class Program
{
static void Main(string[] args)
{
Alarms alarm = new Alarms();
Workers worker = new Workers();
alarm.OnRingSound = worker.getupWorker; //4个问题之q2→事件的赋值
Console.Write("请输入闹钟参数(表示闹钟响的次数):");
uint nClock = Convert.ToUInt16(Console.ReadLine());
alarm.ringAlarm(nClock); //4个问题之q4→触发的调用→f1的调用
Console.ReadLine();
}
}
public class Alarms //2个类之c1:发布者
{
public delegate void RingEvent(); //4个问题之q1→事件的类型
public RingEvent OnRingSound; //1个变量之事件v
public void ringAlarm(uint ringKind) //2个方法之f1:触发事件的方法
{
Console.Write($"闹钟响{ringKind}次了。");
OnRingSound(); //4个问题之q3→事件的触发→f2的调用
}
}
public class Workers //2个类之c2:订阅者
{
public void getupWorker() //2个方法之f2:事件的处理方法
{
Console.WriteLine($"打工人,起床!");
}
}
}
四、为什么使用事件?
在进行事件的赋值时,使用+=符号,而不是直接使用=符号,或者赋值一个空引用,这些操作会对事件的安全性造成威胁。
为了解决这个问题,C#提供了专门的事件处理机制,以保证事件订阅的可靠性。事件通过在委托声明中添加event关键字来实现,如下所示:
public event RingEvent OnRingSound;
此时,以下代码会出现编译错误:
alarm.OnRingSound = worker.getupWorker; // 编译错误:不能直接赋值
alarm.OnRingSound = null; // 编译错误:不能直接赋值为null
而通过订阅事件时,可以使用+=来注册事件处理方法:
alarm.OnRingSound += worker.getupWorker; // 正确:订阅事件
alarm.OnRingSound -= 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); //4个问题之q4→触发的调用→f1的调用
Console.ReadLine();
}
}
public class Alarms //2个类之c1:发布者
{
public delegate void RingEvent(); //4个问题之q1→事件的类型
public event RingEvent OnRingSound; //1个变量之事件v
public void ringAlarm(uint ringKind) //2个方法之f1:触发事件的方法
{
Console.Write($"闹钟响{ringKind}次了。");
OnRingSound(); //4个问题之q3→事件的触发→f2的调用
}
}
public class Workers //2个类之c2:订阅者
{
public void subscribeToRing(Alarms a)
{
a.OnRingSound += getupWorker; //4个问题之q2→事件的赋值
}
public void getupWorker() //2个方法之f2:事件的处理方法
{
Console.WriteLine($"打工人,起床!");
}
}
}
进一步概括
2个类:
发布者、发送者 → c1
订阅者、接收者 → c2
2个方法:
触发事件的方法 → f1(发布者,c1中)
事件的处理方法 → f2(订阅者,c2中)
1个变量:
事件 → v(在c1中定义,在c2中赋值)
4个关键:
事件的类型 → q1 → c1中
事件的赋值 → q2 → c2中
事件的触发 → f2的调用 → q3 → c1中
触发的调用 → f1的调用 → q4 → c1、c2外
发布者的实例化
订阅者的实例化
订阅者订阅
4个问题之q4→触发的调用→f1的调用
2个类之c1:发布者
{
4个问题之q1→事件的类型
1个变量之事件v
2个方法之f1:触发事件的方法
{
4个问题之q3→事件的触发→f2的调用
}
}
2个类之c2:订阅者
{
订阅的方法
{
4个问题之q2→事件的赋值
}
2个方法之f2:事件的处理方法
{
}
}
五、总结
通过这个经典案例“闹钟响了,打工人就要起床了”,我们可以看到事件如何使得C#程序中的各个部分保持松耦合,发布者与订阅者之间并不直接依赖,而是通过事件机制进行通信。这种设计使得我们的代码更加灵活和可扩展。
后续的系统性学习可以看【C#从入门到精通(第6版)】的《17.5 事件》或【叩响C#之门】的《16.4 事件处理机制》。
参考
清华大学出版社-图书详情-《C#从入门到精通(第6版)》
叩响C#之门 (豆瓣)
C# 事件(Event) | 菜鸟教程
C#事件--全网最全+全网最易理解-CSDN博客
ChatGPT | OpenAI