在学习C#的过程中,事件是一个非常重要的知识点。它经常和委托一起出现,因此很多初学者会觉得事件有些抽象,不太容易区分。实际上事件并没有脱离委托,它本质上是,基于委托实现的一种发布-订阅机制。简单来说,事件的作用就是:某个对象发生了一件事,其他对象如果对这件事感兴趣,就可以提前订阅,当事件真的发生时,系统会自动通知这些订阅者。
这套机制在实际开发中非常常见,例如:按钮被点击,文件下载完成,用户登录成功等,这些都可以用事件来实现。
一、什么是事件
事件可以理解为一种对象对外发出的通知 。当某个类中发生了某种情况时,它可以触发一个事件,通知所有已经订阅了这个事件的方法执行相应逻辑。你可以先把事件理解成这样一句话:事件就是"某件事发生了"的消息通知机制。
二、为什么需要事件
很多人学到这里会想,既然委托已经能调用方法了,为什么还需要事件?这是一个非常关键的问题。委托确实可以让我们保存方法并调用方,但如果直接把委托暴露出去,会有一些问题,外部可以直接调用这个委托,外部可以直接清空委托中的方法列表,外部可以随意覆盖原有的订阅关系。这样会导致封装性很差,也不安全。事件就是在委托的基础上增加了一层限制:外部只能订阅和取消订阅,不能随意触发事件。这样就让通知机制变的合理,也更符合面向对象设计思想。
三、事件的基本语法
在C#中,事件使用event关键字声明
语法格式
cs
访问修饰符 event 委托类型 事件名;
例如
cs
public event Action OnCompleted;
或者使用自定义委托:
cs
public delegate void AlarmHandler();
public event AlarmHandler Alarm;
这里的意思是:先声明一个事件,这个事件基于某种委托类型,只有符合这个委托签名的方法才能订阅。
四、事件示例
下面来看一个简答的例子
cs
using System;
public class Publisher
{
public event Action SomethingHappened;
public void DoSomething()
{
Console.WriteLine("正在执行某个操作...");
SomethingHappened?.Invoke();
}
}
public class Program
{
public static void Main()
{
Publisher publisher = new Publisher();
publisher.SomethingHappened += OnSomethingHappened;
publisher.DoSomething();
}
public static void OnSomethingHappened()
{
Console.WriteLine("事件被触发了,收到通知。");
}
}
在这个例子中:
首先声明了
cs
public event Action SomethingHappened;
一个事件SomethingHappened,它是基于Action委托。然后
cs
publisher.SomethingHappened += OnSomethingHappened;
表示把OnSomethingHappened方法注册到事件中。
cs
SomethingHappened?.Invoke();
表示如果有人订阅了这个事件,就依次调用这些订阅的方法。这里的?.Invoke()是安全写法,意思是
- 如果事件不为null,就执行
- 如果没人订阅,事件为null,就不会报错。
五、事件为什么比委托更安全
假设我们这样写:
cs
public Action Notify;
外部不仅可以订阅
cs
obj.Notify += Handler;
还可以:
cs
obj.Notify = null; // 清空所有订阅
obj.Notify = Handler; // 直接覆盖
obj.Notify(); // 外部直接触发
这样就会破坏类的封装性。
使用event后,我们这样改:
cs
public event Action Notify;
那么外部就只能:
cs
obj.Notify += Handler;
obj.Notify -= Handler;
不能直接覆盖赋值,直接清空,直接触发调用,这就保证了事件只能由声明它的类自己控制触发时机。这正是事件的核心价值之一。
六、标准事件模式
在实际的开发中,C#更推荐使用一种标准事件模式。这种模式通常会用到object sender和EventArgs e。
标准模式如下:
cs
public event EventHandler EventName;
或者:
cs
public event EventHandler<TEventArgs> EventName;
下面是一个具体的例子:
cs
using System;
public class Button
{
public event EventHandler Clicked;
public void Click()
{
Console.WriteLine("按钮被点击了");
Clicked?.Invoke(this, EventArgs.Empty);
}
}
public class Program
{
public static void Main()
{
Button button = new Button();
button.Clicked += Button_Clicked;
button.Click();
}
private static void Button_Clicked(object sender, EventArgs e)
{
Console.WriteLine("收到按钮点击事件");
}
}
这里的Clicked是事件,EventHandler是.NET内置委托类型,this表示事件来源,EventArgs.Empty表示没有额外数据。
七、带事件数据的事件
很多时候,事件不仅仅是通知"某件事发生了",还需要携带一些额外信息。例如:
登陆事件需要带用户名,成绩变化事件需要带新的成绩,下载完成事件需要带文件名和大小等。这时就可以自定义事件参数类。
下面给出一个学生成绩的例子
7.1自定义EventArgs
cs
using System;
public class ScoreChangedEventArgs : EventArgs
{
public string StudentName { get; set; }
public int NewScore { get; set; }
public ScoreChangedEventArgs(string studentName, int newScore)
{
StudentName = studentName;
NewScore = newScore;
}
}
7.2使用EventHandler<TEventArgs>
cs
using System;
public class Student
{
public string Name { get; set; }
private int score;
public event EventHandler<ScoreChangedEventArgs> ScoreChanged;
public Student(string name)
{
Name = name;
}
public void UpdateScore(int newScore)
{
score = newScore;
Console.WriteLine($"{Name} 的成绩已更新为 {score}");
ScoreChanged?.Invoke(this, new ScoreChangedEventArgs(Name, newScore));
}
}
public class Program
{
public static void Main()
{
Student student = new Student("张三");
student.ScoreChanged += OnScoreChanged;
student.UpdateScore(95);
}
public static void OnScoreChanged(object sender, ScoreChangedEventArgs e)
{
Console.WriteLine($"收到事件:学生 {e.StudentName} 的新成绩是 {e.NewScore}");
}
}
这个例子就体现了标准事件模式的完整结构:
发布者:Student
事件:ScoreChanged
自定义事件数据:ScoreChangedEventArgs
订阅者:OnScoreChanged
八、事件的触发方式
事件通常在类内部某个操作完成后触发,例如状态改变后触发,数据更新后触发,用户操作发生后触发,某任务完成后触发。
8.1常见触发写法
cs
MyEvent?.Invoke(this, EventArgs.Empty);
这是最常见的安全触发方式。
这里在讲下为啥要用?.Invoke()
因为如果没人订阅事件,那么事件变量就是null。直接调用会报错:
cs
MyEvent(this, EventArgs.Empty); // 可能空引用异常
使用?.Invoke()可以避免这个问题。
九、匿名方法与Lambda订阅事件
在实际开发中,很多事件处理程序并不会单独写成命名方法,而是直接使用匿名方法或Lambda表达式。
9.1使用匿名方法
cs
button.Clicked += delegate (object sender, EventArgs e)
{
Console.WriteLine("匿名方法:按钮被点击");
};
cs
button.Clicked += (sender, e) =>
{
Console.WriteLine("Lambda:按钮被点击");
};
Lambda是现代C#中最常见的写法,简洁且直观。
十、完成案例:温度监控系统
下面是给出的一个完整的案例:温度监控系统,系统要实现监控当前温度,当温度超过警戒值时触发事件,订阅者收到事件后给出提示。
代码实现:
cs
using System;
public class TemperatureExceededEventArgs : EventArgs
{
public double Temperature { get; }
public TemperatureExceededEventArgs(double temperature)
{
Temperature = temperature;
}
}
public class Thermometer
{
public double WarningTemperature { get; set; }
public event EventHandler<TemperatureExceededEventArgs> TemperatureExceeded;
public Thermometer(double warningTemperature)
{
WarningTemperature = warningTemperature;
}
public void CheckTemperature(double currentTemperature)
{
Console.WriteLine($"当前温度:{currentTemperature}℃");
if (currentTemperature > WarningTemperature)
{
TemperatureExceeded?.Invoke(
this,
new TemperatureExceededEventArgs(currentTemperature)
);
}
}
}
public class Alarm
{
public void OnTemperatureExceeded(object sender, TemperatureExceededEventArgs e)
{
Console.WriteLine($"警报:温度超标,当前温度为 {e.Temperature}℃!");
}
}
public class Program
{
public static void Main()
{
Thermometer thermometer = new Thermometer(30);
Alarm alarm = new Alarm();
thermometer.TemperatureExceeded += alarm.OnTemperatureExceeded;
thermometer.CheckTemperature(25);
thermometer.CheckTemperature(32);
}
}
这个案例中的:
发布者:Thermometer
事件:TemperatureExceeded
事件数据:TemperatureExceededEventArgs
订阅者:Alarm
这里Thermometer不需要知道谁来处理温度超标,它只负责在条件满足时触发事件。而处理逻辑由外部订阅者决定,这就实现了解耦。
十一、事件与委托的区别总结
|-----------|---------|-------------|
| 对比项 | 委托 | 事件 |
| 本质 | 引用方法的类型 | 基于委托的通知机制 |
| 外部是否可直接调用 | 可以 | 不可以 |
| 外部是否可覆盖 | 可以 | 不可以直接覆盖 |
| 主要用途 | 传递方法、回调 | 发布-订阅通知 |
| 使用方式 | 赋值、调用 | +=订阅、-=取消订阅 |
总结成一句话:委托偏重方法传递,事件偏重消息通知
十二、参考链接
-
Microsoft Learn - 事件(C# 编程指南)
https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/events/ -
Microsoft Learn - 如何声明和引发符合 .NET 指南的事件
https://learn.microsoft.com/zh-cn/dotnet/standard/events/how-to-raise-and-consume-events