C# 事件使用详解
总目录
前言
在C#中,事件(Events)是一种基于委托的重要机制,用于实现对象之间的松耦合通信。它通过发布-订阅模式(Publisher-Subscriber Pattern),允许一个对象(发布者)在特定条件发生时通知其他对象(订阅者)执行相应操作。事件是构建响应式、动态应用程序的核心工具,广泛应用于UI交互、游戏开发、网络通信等领域。本文将全面详细地介绍C#事件的使用,包括事件的定义、发布、订阅、取消订阅以及事件的高级用法。
一、什么是事件?
1. 定义
事件是一种特殊的委托(Delegate),用于通知其他对象某个状态或操作的发生。它通过委托实现,但提供了更安全的封装机制。
本质上 事件就是委托,如果说委托是对方法的包装,那么事件就是对委托进一步的包装,提升了使用的安全性,事件是安全版的委托
2. 定义语法
在 C# 中,事件的定义语法如下:
public event DelegateType EventName;
DelegateType
:委托类型,定义了事件的签名。EventName
:事件的名称。
3. 完整生命周期
4. 关键术语
事件是 C# 中实现发布-订阅模式的核心机制,允许对象在特定动作发生时通知其他对象,实现松耦合的通信。 相关关键术语如下:
-
发布者(Publisher):定义事件并触发事件的对象,负责通知事件的发生,如按钮控件。
-
订阅者(Subscriber):订阅(监听)事件并定义事件处理逻辑的对象,当事件触发时执行相应操作。
-
事件处理程序(Event Handler):订阅者注册的方法,用于处理事件触发后的逻辑。
-
事件参数(EventArgs):传递事件相关数据的载体。
-
触发(Raise)事件:发布者调用事件,通知所有订阅者执行其处理程序。
5. 理解发布订阅模式
现实世界的类比,想象一个报社订阅系统:
- 报社(发布者):定期发布报纸,无需关心订阅者的具体身份;
- 读者(订阅者):通过订阅获得报纸,根据内容采取行动(如阅读、剪报)。
- 这种“一对多”的通信模式,正是事件的核心—发布-订阅模式(Publish-Subscribe Pattern)。
6. 事件与委托的关系
- 委托定义了事件处理方法的签名,类似于函数指针。
- 事件:安全封装的委托
- 事件(Event):基于委托的封装,通过
event
关键字实现,是安全版委托。原因如下:- 外部不可直接调用和触发事件:仅发布者类可触发事件(
event?.Invoke()
); - 订阅/取消订阅控制:外部只允许通过
+=
和-=
操作管理订阅者列表。
- 外部不可直接调用和触发事件:仅发布者类可触发事件(
特性 | 委托(Delegate) | 事件(Event) |
---|---|---|
定义语法 | 通过 delegate 关键字定义,并可以直接调用。 | 通过 event 关键字定义,并且只能在类内部触发事件。 |
访问权限 | 外部代码可以直接调用委托。 | 通过 event 关键字封装,外部只能通过 += 和 -= 操作。 |
安全性 | 无封装,可能存在被滥用的风险。 | 提供更安全的封装,避免外部随意修改委托链。 |
多播支持 | 支持多播,可直接添加或移除方法。 | 内部基于委托实现多播,但对外部隐藏委托实例。 |
设计目的 | 通用方法引用的封装。 | 专为发布-订阅模式设计,实现对象间解耦。 |
使用场景 | 需要灵活调用不同方法的场景, 例如回调函数、异步编程等。 | 需要发布-订阅模式的场景, 例如用户界面交互、状态变化通知等。 |
7. 事件的基本认识
1)事件的定义
事件是基于委托的一种机制,用于在对象之间传递消息。要定义一个事件,首先需要定义一个委托类型,然后使用event
关键字来声明事件。
public delegate void EventHandler(object sender, EventArgs e);
public class EventPublisher
{
public event EventHandler MyEvent;
}
2)事件的发布
事件的发布是指在特定条件下触发事件,通知所有订阅者。这通常通过调用事件的Invoke
方法来实现。
public class EventPublisher
{
public event EventHandler MyEvent;
public void RaiseEvent()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}
}
3)事件的订阅
事件的订阅是指订阅者注册到事件上,以便在事件触发时接收通知。这通常通过将一个方法传递给事件来实现。
public class EventSubscriber
{
public void OnEvent(object sender, EventArgs e)
{
Console.WriteLine("Event received!");
}
}
public class Program
{
public static void Main()
{
EventPublisher publisher = new EventPublisher();
EventSubscriber subscriber = new EventSubscriber();
publisher.MyEvent += subscriber.OnEvent; // 订阅事件
publisher.RaiseEvent(); // 触发事件
}
}
4)事件的取消订阅
事件的取消订阅是指订阅者从事件中注销,不再接收通知。这通常通过使用-=
运算符来实现。
public class Program
{
public static void Main()
{
EventPublisher publisher = new EventPublisher();
EventSubscriber subscriber = new EventSubscriber();
publisher.MyEvent += subscriber.OnEvent; // 订阅事件
publisher.RaiseEvent(); // 触发事件
publisher.MyEvent -= subscriber.OnEvent; // 取消订阅事件
publisher.RaiseEvent(); // 不再触发事件
}
}
二、如何使用事件
1. 自定义事件
1) 使用示例
using System;
// 1. 委托定义(签名规范)
public delegate void MessageHandler(string msg);
// Sender 类负责发布消息
public class Sender
{
// 2. 事件声明
public event MessageHandler OnMessageReceived;
// 3. 事件触发方法
protected virtual void RaiseEvent(string message)
{
OnMessageReceived?.Invoke(message);
}
// 发送消息:在发送消息时调用 RaiseEvent 来触发事件。
public void SendMessage(string message)
{
Console.WriteLine($"Publisher: Sending message '{message}'");
RaiseEvent(message);
}
}
// Subscriber 类负责接收和处理消息
public class Receiver
{
private string _name;
public Receiver(string name)
{
_name = name;
}
public void HandleMessage(string message)
{
Console.WriteLine($"{_name} received message: {message}");
}
}
class Program
{
static void Main(string[] args)
{
// 创建 Sender 实例
var sender = new Sender();
// 创建多个 Receiver 实例
var receiver1 = new Receiver("Receiver 1");
var receiver2 = new Receiver("Receiver 2");
// 订阅事件
sender.OnMessageReceived += receiver1.HandleMessage;
sender.OnMessageReceived += receiver2.HandleMessage;
// 发送消息并触发事件
sender.SendMessage("Hello, World!");
/*
输出:
Publisher: Sending message 'Hello, World!'
Receiver 1 received message: Hello, World!
Receiver 2 received message: Hello, World!
*/
sender.SendMessage("Another message!");
/*
输出:
Publisher: Sending message 'Another message!'
Receiver 1 received message: Another message!
Receiver 2 received message: Another message!
*/
// 取消订阅某个事件(可选)
sender.OnMessageReceived -= receiver1.HandleMessage;
// 再次发送消息并触发事件
sender.SendMessage("This message will not reach Receiver 1");
/*
输出:
Publisher: Sending message 'This message will not reach Receiver 1'
Receiver 2 received message: This message will not reach Receiver 1
*/
}
}
2)自定义事件使用步骤
以上示例展示了如何定义委托、声明事件、触发事件以及如何订阅和处理事件。下面拆解一下自定义事件的使用步骤。
1. 委托定义
我们定义了一个名为 MessageHandler
的委托类型,它接受一个字符串参数 msg
并返回 void
。
public delegate void MessageHandler(string msg);
2. 事件声明
在 Sender
类中,我们声明了一个名为 OnMessageReceived
的事件,该事件使用 MessageHandler
委托类型。
public event MessageHandler OnMessageReceived;
3. 定义事件触发方法
我们定义了一个 RaiseEvent
方法,用于触发事件。该方法检查是否有订阅者,并调用他们的处理方法。
protected virtual void RaiseEvent(string message)
{
OnMessageReceived?.Invoke(message);
}
4. 触发事件时机
在适当的地方调用 RaiseEvent
方法来触发事件。SendMessage
方法用于发送消息,并在发送消息时调用 RaiseEvent
方法来触发事件。
public void SendMessage(string message)
{
Console.WriteLine($"Publisher: Sending message '{message}'");
RaiseEvent(message);
}
5. 定义处理事件方法
每个 Subscriber
实例都有一个 HandleMessage
方法,用于处理接收到的消息。
public void HandleMessage(string message)
{
Console.WriteLine($"{_name} received message: {message}");
}
6. 订阅事件
在 Program
类的 Main
方法中,我们创建了 Publisher
和 Subscriber
实例,并通过 +=
运算符订阅事件。
var publisher = new Publisher();
var subscriber1 = new Subscriber("Subscriber 1");
var subscriber2 = new Subscriber("Subscriber 2");
publisher.OnMessageReceived += subscriber1.HandleMessage;
publisher.OnMessageReceived += subscriber2.HandleMessage;
7. 发送消息并触发事件
我们调用 SendMessage
方法来发送消息,并触发事件。
publisher.SendMessage("Hello, World!");
publisher.SendMessage("Another message!");
8. 取消订阅事件(可选)
我们可以通过 -=
运算符 取消订阅某个事件,以停止接收通知。
publisher.OnMessageReceived -= subscriber1.HandleMessage;
3) 核心步骤
// 1. 委托定义(签名规范)
public delegate void MessageHandler(string msg);
// 2. 事件声明
public event MessageHandler OnMessageReceived;
// 3. 事件触发方法
protected virtual void RaiseEvent(string message)
{
OnMessageReceived?.Invoke(message);
}
4)简单示例
1. 定义事件
public class Button
{
// 定义委托类型
public delegate void ClickHandler(object sender, EventArgs e);
// 定义事件
public event ClickHandler Click;
}
2. 触发事件
public class Button
{
public delegate void ClickHandler(object sender, EventArgs e);
public event ClickHandler Click;
public void OnClick()
{
// 触发事件
Click?.Invoke(this, EventArgs.Empty);
}
}
3. 订阅事件
public class Program
{
public static void Main()
{
Button button = new Button();
// 订阅事件
button.Click += Button_Click;
}
private static void Button_Click(object sender, EventArgs e)
{
Console.WriteLine("Button clicked!");
}
}
2. EventHandler
在C#中,EventHandler
是一个预定义的委托类型,用于处理事件。它是 .NET Framework 中事件系统的一个重要组成部分,简化了事件的定义和使用过程。
1)定义
EventHandler
是一个预定义的委托类型,它被设计用来处理不带任何额外数据的事件。其签名如下:
public delegate void EventHandler(object sender, EventArgs e);
object sender
:表示触发事件的对象。EventArgs e
:包含事件数据的对象。对于EventHandler
,这个参数通常是EventArgs.Empty
,因为它不携带任何特定的数据。
2)用途
EventHandler
主要用于那些不需要传递额外信息的事件。例如,按钮点击事件通常只需要知道哪个控件触发了事件,而不需要其他详细信息。
3)使用步骤
1. 定义委托类型(省略)
从C# 2.0开始,可以直接使用预定义的 EventHandler
委托类型,无需手动定义。
2. 定义事件及触发事件的方法
接下来,在类中定义一个事件。使用 event
关键字来声明事件,并指定其对应的委托类型。
public class Publisher
{
// 定义一个事件
public event EventHandler MyEvent;
// 触发事件的方法
protected virtual void OnMyEvent(EventArgs e)
{
MyEvent?.Invoke(this, e);
}
}
3. 触发事件时机
在适当的地方触发事件,通常是当某个条件满足时。为了触发事件,我们调用 OnMyEvent
方法,并传入适当的参数。
public class Publisher
{
public event EventHandler MyEvent;
protected virtual void OnMyEvent(EventArgs e)
{
MyEvent?.Invoke(this, e);
}
public void DoSomething()
{
Console.WriteLine("Doing something...");
// 某些操作完成后触发事件
OnMyEvent(EventArgs.Empty);
}
}
4. 定义事件处理方法
public class Subscriber
{
public void HandleEvent(object sender, EventArgs e)
{
Console.WriteLine("Event received!");
}
}
5. 订阅事件与触发事件
在需要接收事件通知的对象中订阅事件。这通常通过 +=
运算符来完成。
public class Program
{
public static void Main(string[] args)
{
var publisher = new Publisher();
var subscriber = new Subscriber();
// 订阅事件
publisher.MyEvent += subscriber.HandleEvent;
// 触发事件
publisher.DoSomething();
}
}
4)完整代码示例
using System;
public class Publisher
{
// 定义 事件
public event EventHandler MyEvent;
// 定义 触发事件的方法
protected virtual void OnMyEvent(EventArgs e)
{
MyEvent?.Invoke(this, e);
}
// 触发事件
public void DoSomething()
{
Console.WriteLine("Doing something...");
// 某些操作完成后触发事件
OnMyEvent(EventArgs.Empty);
}
}
public class Subscriber
{
// 处理事件
public void HandleEvent(object sender, EventArgs e)
{
Console.WriteLine("Event received!");
}
}
public class Program
{
public static void Main(string[] args)
{
var publisher = new Publisher();
var subscriber = new Subscriber();
// 订阅事件
publisher.MyEvent += subscriber.HandleEvent;
// 触发事件
publisher.DoSomething();
}
}
运行结果:
Doing something...
Event received!
3. EventHandler<TEventArgs>
1)定义
EventHandler<TEventArgs>
是一个泛型委托类型,用于处理带有特定事件数据的事件。它允许你传递额外的信息给事件处理程序,从而增强事件机制的功能。它的签名如下:
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs : EventArgs;
object sender
:表示触发事件的对象。TEventArgs e
:包含事件数据的对象,必须继承自EventArgs
类。允许你传递更多的信息给事件处理程序。
2)用途
EventHandler<TEventArgs>
主要用于那些需要传递额外信息的事件。通过使用泛型参数 TEventArgs
,你可以指定一个具体的 EventArgs
子类来携带更多详细信息。
💡 提示:若无需自定义参数,可直接使用预定义的
EventHandler
委托。
3)使用步骤
假设我们正在开发一个温度传感器应用程序。当温度发生变化时,传感器会通知所有订阅者当前的温度值。
1. 定义自定义 EventArgs
首先,我们需要定义一个继承自 EventArgs
的类,用于携带温度变化的具体信息。
public class TemperatureChangedEventArgs : EventArgs
{
public double NewTemperature { get; }
public TemperatureChangedEventArgs(double newTemperature)
{
NewTemperature = newTemperature;
}
}
2. 定义发布者类(Publisher)
接下来,我们定义一个 TemperatureSensor
类,它负责发布温度变化事件。
public class TemperatureSensor
{
// 使用 EventHandler<TEventArgs> 声明事件
public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
private double _temperature;
public double Temperature
{
get => _temperature;
set
{
if (_temperature != value)
{
_temperature = value;
OnTemperatureChanged(new TemperatureChangedEventArgs(_temperature));
}
}
}
protected virtual void OnTemperatureChanged(TemperatureChangedEventArgs e)
{
TemperatureChanged?.Invoke(this, e);
}
}
在这个类中:
- 我们声明了一个
TemperatureChanged
事件,使用了EventHandler<TemperatureChangedEventArgs>
委托类型。 - 在设置
Temperature
属性时,如果新的温度值与旧值不同,则触发TemperatureChanged
事件。
3. 定义订阅者类(Subscriber)
现在,我们定义一个 Thermostat
类,它将订阅并处理温度变化事件。
public class Thermostat
{
public void HandleTemperatureChange(object sender, TemperatureChangedEventArgs e)
{
Console.WriteLine($"Temperature changed to {e.NewTemperature}°C");
}
}
在这个类中,我们定义了一个 HandleTemperatureChange
方法,该方法将作为事件处理程序。
4. 主程序(订阅+触发事件)
最后,在主程序中,我们将创建 TemperatureSensor
和 Thermostat
实例,并订阅事件。
using System;
public class Program
{
static void Main(string[] args)
{
var sensor = new TemperatureSensor();
var thermostat = new Thermostat();
// 订阅温度变化事件
sensor.TemperatureChanged += thermostat.HandleTemperatureChange;
// 改变温度值
sensor.Temperature = 25.5;
sensor.Temperature = 26.0;
// 取消订阅事件(可选)
sensor.TemperatureChanged -= thermostat.HandleTemperatureChange;
// 再次改变温度值
sensor.Temperature = 27.0;
}
}
输出结果
运行上述代码后,控制台输出将如下所示:
Temperature changed to 25.5°C
Temperature changed to 26°C
4)完整代码示例
using System;
public class TemperatureChangedEventArgs : EventArgs
{
public double NewTemperature { get; }
public TemperatureChangedEventArgs(double newTemperature)
{
NewTemperature = newTemperature;
}
}
public class TemperatureSensor
{
// 使用 EventHandler<TEventArgs> 声明事件
public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
private double _temperature;
public double Temperature
{
get => _temperature;
set
{
if (_temperature != value)
{
_temperature = value;
OnTemperatureChanged(new TemperatureChangedEventArgs(_temperature));
}
}
}
protected virtual void OnTemperatureChanged(TemperatureChangedEventArgs e)
{
TemperatureChanged?.Invoke(this, e);
}
}
public class Thermostat
{
public void HandleTemperatureChange(object sender, TemperatureChangedEventArgs e)
{
Console.WriteLine($"Temperature changed to {e.NewTemperature}°C");
}
}
public class Program
{
static void Main(string[] args)
{
var sensor = new TemperatureSensor();
var thermostat = new Thermostat();
// 订阅温度变化事件
sensor.TemperatureChanged += thermostat.HandleTemperatureChange;
// 改变温度值
sensor.Temperature = 25.5;
sensor.Temperature = 26.0;
// 取消订阅事件(可选)
sensor.TemperatureChanged -= thermostat.HandleTemperatureChange;
// 再次改变温度值
sensor.Temperature = 27.0;
}
}
EventHandler
:是一个预定义的委托类型,适用于不需要传递额外信息的事件。其签名是void EventHandler(object sender, EventArgs e)
。EventHandler<TEventArgs>
:是一个泛型委托类型,适用于需要传递额外信息的事件。它允许你指定一个派生自EventArgs
的类型作为参数,从而传递更多数据。
4. 订阅事件的方式
1)订阅方式
- 使用事件处理程序(处理事件的方法)订阅事件
- 使用匿名方法订阅事件
- 使用 Lambda 表达式订阅事件
2)订阅示例
using System;
public class DownloadCompletedEventArgs : EventArgs
{
public string FileName { get; }
public long FileSize { get; }
public DownloadCompletedEventArgs(string name, long size)
{
FileName = name;
FileSize = size;
}
}
public class Downloader
{
// 使用泛型EventHandler
public event EventHandler<DownloadCompletedEventArgs> DownloadCompleted;
protected virtual void OnDownloadCompleted(DownloadCompletedEventArgs e)
{
DownloadCompleted?.Invoke(this, e);
}
public void StartDownload(DownloadCompletedEventArgs eventArgs)
{
Console.WriteLine("StartDownload...");
// 模拟下载
Thread.Sleep(2000);
OnDownloadCompleted(eventArgs);
}
}
class Logger
{
public void LogDownload(object sender, DownloadCompletedEventArgs eventArgs)
{
Console.WriteLine($"订阅方式-方法赋值:[{DateTime.Now}] {eventArgs.FileName}文件 {eventArgs.FileSize} 下载完成");
}
}
public class Program
{
static void Main(string[] args)
{
var downloader = new Downloader();
var logger = new Logger();
// 日志类 订阅下载完成事件
// 订阅方式1:使用方法赋值
downloader.DownloadCompleted += logger.LogDownload!;
// 订阅方式2:使用匿名方法
downloader.DownloadCompleted += delegate (object sender, DownloadCompletedEventArgs eventArgs)
{
Console.WriteLine($"订阅方式-匿名方法:[{DateTime.Now}] {eventArgs.FileName}文件 {eventArgs.FileSize} 下载完成");
}!;
// 订阅方式3:使用 Lambda 表达式
downloader.DownloadCompleted += (sender, eventArgs) =>
{
Console.WriteLine($"订阅方式 - Lambda 表达式:[{DateTime.Now}] {eventArgs.FileName}文件 {eventArgs.FileSize} 下载完成");
}!;
// 触发事件
downloader.StartDownload(new DownloadCompletedEventArgs("file", 1224));
}
}
运行结果:
StartDownload...
订阅方式-方法赋值:[2025/3/12 16:02:41] file文件 1224 下载完成
订阅方式-匿名方法:[2025/3/12 16:02:41] file文件 1224 下载完成
订阅方式 - Lambda 表达式:[2025/3/12 16:02:41] file文件 1224 下载完成
5. 事件访问器
事件访问器(Event Accessors)允许我们在订阅或取消订阅事件时执行自定义逻辑。通过重写 add
和 remove
访问器,可以在事件订阅和取消订阅时进行额外的操作。
public class Publisher
{
private EventHandler _myEvent;
public event EventHandler MyEvent
{
add
{
Console.WriteLine($"添加订阅: {value.Method.Name}");
_myEvent += value;
}
remove
{
Console.WriteLine($"移除订阅: {value.Method.Name}");
_myEvent -= value;
}
}
protected virtual void OnMyEvent(EventArgs e)
{
_myEvent?.Invoke(this, e);
}
public void DoSomething()
{
Console.WriteLine("Doing something...");
OnMyEvent(EventArgs.Empty);
}
}
public class Subscriber
{
public void HandleEvent(object sender, EventArgs e)
{
Console.WriteLine("Event received!");
}
}
public class Program
{
public static void Main(string[] args)
{
var publisher = new Publisher();
var subscriber = new Subscriber();
// 订阅事件
publisher.MyEvent += subscriber.HandleEvent;
// 触发事件
publisher.DoSomething();
// 取消订阅事件
publisher.MyEvent -= subscriber.HandleEvent;
}
}
运行结果:
添加订阅: HandleEvent
Doing something...
Event received!
移除订阅: HandleEvent
6. 静态事件
类级别的事件,由类本身触发和订阅:
public class Logger
{
public static event EventHandler GlobalLogEvent;
public static void Log(string message)
{
GlobalLogEvent?.Invoke(null, new LogEventArgs(message));
}
}
// 订阅静态事件
Logger GlobalLogEvent += (s, e) => Console.WriteLine($"全局日志:{e.Message}");
四、事件应用场景
1. 主要应用场景
- 用户界面交互:处理用户的输入和操作,如点击按钮、选择菜单项等。
- 状态变化通知:当某个对象的状态发生变化时,通知其他依赖的对象。
- 异步编程:在异步操作完成时通知调用方。
2. 应用场景示例
示例1:WinForms/WPF中的事件
UI框架大量使用事件机制(如 Button.Click
、TextBox.TextChanged
),通过XAML或代码绑定处理程序,实现用户交互响应。
// C#代码响应事件
private void Button_Click(object sender, RoutedEventArgs e)
{
Console.WriteLine("点击事件触发!");
}
示例2:状态变化通知
当某个对象的状态发生变化时,可以通过事件通知其他依赖的对象。
public class TemperatureSensor
{
public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
private double _temperature;
public double Temperature
{
get => _temperature;
set
{
if (_temperature != value)
{
_temperature = value;
OnTemperatureChanged(new TemperatureChangedEventArgs(_temperature));
}
}
}
protected virtual void OnTemperatureChanged(TemperatureChangedEventArgs e)
{
TemperatureChanged?.Invoke(this, e);
}
}
public class TemperatureChangedEventArgs : EventArgs
{
public double NewTemperature { get; }
public TemperatureChangedEventArgs(double newTemperature)
{
NewTemperature = newTemperature;
}
}
public class Thermostat
{
public void HandleTemperatureChange(object sender, TemperatureChangedEventArgs e)
{
Console.WriteLine($"Temperature changed to {e.NewTemperature}°C");
}
}
public class Program
{
public static void Main(string[] args)
{
var sensor = new TemperatureSensor();
var thermostat = new Thermostat();
// 订阅温度变化事件
sensor.TemperatureChanged += thermostat.HandleTemperatureChange;
// 改变温度值
sensor.Temperature = 25.5;
sensor.Temperature = 26.0;
}
}
示例3: 异步编程
事件还可以用于异步操作完成时通知调用方。
using System;
using System.Threading.Tasks;
public class TaskRunner
{
public event EventHandler<TaskCompletedEventArgs> TaskCompleted;
public async Task RunTaskAsync()
{
Console.WriteLine("Starting task...");
await Task.Delay(2000); // 模拟耗时操作
Console.WriteLine("Task completed.");
OnTaskCompleted(new TaskCompletedEventArgs(true, "Task finished successfully."));
}
protected virtual void OnTaskCompleted(TaskCompletedEventArgs e)
{
TaskCompleted?.Invoke(this, e);
}
}
public class TaskCompletedEventArgs : EventArgs
{
public bool Success { get; }
public string Message { get; }
public TaskCompletedEventArgs(bool success, string message)
{
Success = success;
Message = message;
}
}
public class Program
{
public static async Task Main(string[] args)
{
var runner = new TaskRunner();
// 订阅任务完成事件
runner.TaskCompleted += (sender, e) =>
{
Console.WriteLine($"Task result: {e.Success}, Message: {e.Message}");
};
// 执行异步任务
await runner.RunTaskAsync();
}
}
运行结果:
Starting task...
Task completed.
Task result: True, Message: Task finished successfully.
示例4:设计模式实践
在MVVM架构中,事件常与命令模式结合使用:
public class RelayCommand : ICommand
{
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
这种模式有效分离UI逻辑与业务逻辑。
五、使用须知
1)避免内存泄漏
事件订阅会导致订阅者对象无法被垃圾回收,除非显式取消订阅。因此,确保在不再需要监听事件时取消订阅。
事件处理程序会保持对订阅对象的引用,这可能会影响垃圾回收。应确保在不再需要事件处理程序时及时取消订阅。
public class Program
{
public static void Main(string[] args)
{
var publisher = new Publisher();
var subscriber = new Subscriber();
// 订阅事件
publisher.MyEvent += subscriber.HandleEvent;
// 触发事件
publisher.DoSomething();
// 取消订阅事件
publisher.MyEvent -= subscriber.HandleEvent;
}
}
确保在订阅者不再需要时取消订阅事件,防止对象被意外保留:
public class Subscriber
{
private EventPublisher _publisher;
public Subscriber(EventPublisher publisher)
{
_publisher = publisher;
_publisher.MyEvent += HandleEvent;
}
public void Dispose()
{
_publisher.MyEvent -= HandleEvent; // 取消订阅
}
}
2)线程安全
在多线程环境中,事件的订阅和触发可能会引发线程安全问题。可以使用锁机制或其他同步手段来确保线程安全。
public class Publisher
{
private EventHandler _myEvent;
private readonly object _lockObject = new object();
public event EventHandler MyEvent
{
add
{
lock (_lockObject)
{
_myEvent += value;
}
}
remove
{
lock (_lockObject)
{
_myEvent -= value;
}
}
}
protected virtual void OnMyEvent(EventArgs e)
{
lock (_lockObject)
{
_myEvent?.Invoke(this, e);
}
}
public void DoSomething()
{
Console.WriteLine("Doing something...");
OnMyEvent(EventArgs.Empty);
}
}
// 安全触发方式
var localCopy = TheEvent;
localCopy?.Invoke(this, args);
3)空事件检查
避免空引用异常,推荐使用空条件运算符(?.
):
// 错误示例:未检查事件是否为空
if (MyEvent != null) MyEvent(this, EventArgs.Empty);
// 优化写法(C# 6.0+)
MyEvent?.Invoke(this, EventArgs.Empty);
4)事件处理程序的性能
避免在频繁触发的事件中执行耗时操作,另外频繁地发布和订阅事件可能会影响性能,特别是在事件处理程序较多的情况下。应尽量减少不必要的事件发布和订阅。
5)性能优化
高频触发事件时,考虑使用 WeakEventManager
防止内存泄漏。
2. 事件的优势
1)解耦发布者和订阅者
事件允许发布者和订阅者之间松散耦合,提高代码的可维护性和可扩展性。
2)支持多个订阅者
一个事件可以有多个订阅者,每个订阅者都可以定义自己的处理逻辑。
3)灵活的通信机制
事件提供了一种灵活的通信机制,适用于各种场景,如 UI
交互、后台任务完成通知等。
3. 最佳实践
1)使用 EventHandler<TEventArgs>
处理自定义事件
对于自定义事件,建议使用 EventHandler<TEventArgs>
作为事件委托类型。这样可以使你的事件处理机制更加一致和易于理解。
避免重复定义委托类型,提升代码简洁性。
// 自定义事件参数
public class TemperatureChangedEventArgs : EventArgs
{
public int NewTemperature { get; set; }
}
// 事件声明
public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
// 触发事件时传递数据
protected virtual void OnTemperatureChanged(int newTemp)
{
TemperatureChanged?.Invoke(
this,
new TemperatureChangedEventArgs { NewTemperature = newTemp }
);
}
2)遵循命名约定
- 事件的命名应遵循 C# 的命名约定,如使用 “
Event
” 为后缀 或TemperatureChanged
、TaskCompleted
等形式作为事件的名称。 - 以
On
或Raise
为前缀命名触发方法,确保参数合法性检查。
// 触发事件的方法
protected virtual void OnMyEvent(EventArgs e)
{
MyEvent?.Invoke(this, e);
}
3)避免过度使用事件
虽然事件非常灵活,但在某些情况下,过度使用事件可能会导致代码难以阅读和维护。
4. 常见问题与解答
Q1. 事件能否被继承或重写?
事件可以被继承,但需通过 override
关键字重写事件的访问器:
public class BaseClass {
public virtual event EventHandler MyEvent;
}
public class DerivedClass : BaseClass
{
public override event EventHandler MyEvent
{
add { base.MyEvent += value; }
remove { base.MyEvent -= value; }
}
}
Q2. 如何实现事件的多播顺序控制?
事件的多播顺序由订阅顺序决定,无法直接修改。若需自定义顺序,需手动管理委托链:
// 手动管理委托链
private MyEventHandler _customHandlers;
public event MyEventHandler CustomEvent
{
add { _customHandlers += value; }
remove { _customHandlers -= value; }
}
// 触发时按逆序执行(例如优先执行高优先级方法)
protected virtual void OnCustomEvent()
{
if (_customHandlers != null)
{
var handlers = _customHandlers.GetInvocationList().Reverse();
foreach (Delegate handler in handlers)
{
((MyEventHandler)handler)(this, EventArgs.Empty);
}
}
}
4. 小结
C#事件是实现松耦合、响应式编程的核心机制,其核心在于委托的封装和发布-订阅模式的灵活应用。通过事件,开发者可以:
- 解耦对象:发布者无需知道订阅者具体是谁。
- 动态响应:在事件发生时立即执行相关逻辑。
- 扩展性:轻松添加或移除事件处理程序。
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
MSDN文档 - 事件