当前位置: 首页 > news >正文

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 方法中,我们创建了 PublisherSubscriber 实例,并通过 += 运算符订阅事件。

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. 主程序(订阅+触发事件)

最后,在主程序中,我们将创建 TemperatureSensorThermostat 实例,并订阅事件。

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
{// 使用泛型EventHandlerpublic 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)允许我们在订阅或取消订阅事件时执行自定义逻辑。通过重写 addremove 访问器,可以在事件订阅和取消订阅时进行额外的操作。

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.ClickTextBox.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” 为后缀 或 TemperatureChangedTaskCompleted 等形式作为事件的名称。
  • OnRaise为前缀命名触发方法,确保参数合法性检查。
// 触发事件的方法
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#事件是实现松耦合、响应式编程的核心机制,其核心在于委托的封装发布-订阅模式的灵活应用。通过事件,开发者可以:

  1. 解耦对象:发布者无需知道订阅者具体是谁。
  2. 动态响应:在事件发生时立即执行相关逻辑。
  3. 扩展性:轻松添加或移除事件处理程序。

结语

回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


参考资料:
MSDN文档 - 事件

相关文章:

C# 事件使用详解

总目录 前言 在C#中&#xff0c;事件&#xff08;Events&#xff09;是一种基于委托的重要机制&#xff0c;用于实现对象之间的松耦合通信。它通过发布-订阅模式&#xff08;Publisher-Subscriber Pattern&#xff09;&#xff0c;允许一个对象&#xff08;发布者&#xff09;…...

flink cdc同步mysql数据

一、api 添加依赖 <dependency><groupId>org.apache.flink</groupId><artifactId>flink-connector-mysql-cdc</artifactId><!-- 请使用已发布的版本依赖&#xff0c;snapshot 版本的依赖需要本地自行编译。 --><version>3.3-SNAP…...

tomcat负载均衡配置

这里拿Nginx和之前做的Tomcat 多实例来实现tomcat负载均衡 1.准备多实例与nginx tomcat单机多实例部署-CSDN博客 2.配置nginx做负载均衡 upstream tomcat{ server 192.168.60.11:8081; server 192.168.60.11:8082; server 192.168.60.11:8083; } ser…...

Ceph(1):分布式存储技术简介

1 分布式存储技术简介 1.1 分布式存储系统的特性 &#xff08;1&#xff09;可扩展 分布式存储系统可以扩展到几百台甚至几千台的集群规模&#xff0c;而且随着集群规模的增长&#xff0c;系统整体性能表现为线性增长。分布式存储的水平扩展有以下几个特性&#xff1a; 节点…...

16、JavaEE核心技术-EL与 JSTL

EL与 JSTL 实践 一. EL&#xff08;Expression Language&#xff09; EL&#xff08;表达式语言&#xff09;是 JSP 2.0 中引入的一种简单的脚本语言&#xff0c;用于在 JSP 页面中简化数据的访问和显示。它通过一种类似于 JavaScript 的语法&#xff0c;允许开发者在 JSP 页面…...

RabbitMQ报错:Shutdown Signal channel error; protocol method

报错信息&#xff1a; Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code406, reply-textPRECONDITION_FAILED - unknown delivery tag 1, class-id60, method-id80) 原因 默认情况下 RabbitMQ 是自动ACK&#xff08;确认签收&…...

使用DeepSeek完成一个简单嵌入式开发

开启DeepSeek对话 请帮我使用Altium Designer设计原理图、PCB&#xff0c;使用keil完成代码编写&#xff1b;要求&#xff1a;使用stm32F103RCT6为主控芯片&#xff0c;控制3个流水灯的原理图 这里需要注意&#xff0c;每次DeepSeek的回答都不太一样。 DeepSeek回答 以下是使…...

NLP技术介绍

NLP技术介绍 语言分析技术分词词性标注命令实体识别句法分析语义分析文本处理技术文本分类文本聚类情感分析文本生成机器翻译对话系统与交互技术聊天机器人问答系统语音识别与合成知识图谱与语义理解技术知识图谱语义搜索语义推理深度学习与预训练模型循环神经网络(RNN)及其变…...

pycharm + anaconda + yolo11(ultralytics) 的视频流实时检测,保存推流简单实现

目录 背景pycharm安装配置代码实现创建本地视频配置 和 推流配置视频帧的处理和检测框绘制主要流程遇到的一些问题 背景 首先这个基于完整安装配置了anaconda和yolo11的环境&#xff0c;如果需要配置开始的话&#xff0c;先看下专栏里另一个文章。 这次的目的是实现拉取视频流…...

C++编译问题——1模板函数的实现必须在头文件中

今天编译数据结构时&#xff0c;遇见一个编译错误 假设你有一个头文件 SeqList.h 和一个源文件 SeqList.cpp。 SeqList.h #ifndef SEQLIST_H #define SEQLIST_H#include <stdexcept> #include <iostream>template<typename T> class SeqList { private:sta…...

深度学习PyTorch之数据加载DataLoader

深度学习pytorch之简单方法自定义9类卷积即插即用 文章目录 数据加载基础架构1、Dataset类详解2、DataLoader核心参数解析3、数据增强 数据加载基础架构 核心类关系图 torch.utils.data ├── Dataset (抽象基类) ├── DataLoader (数据加载器) ├── Sampler (采样策略)…...

使用Beanshell前置处理器对Jmeter的请求body进行加密

这里我们用HmacSHA256来进行加密举例&#xff1a; 步骤&#xff1a; 1.先获取请求参数并对请求参数进行处理&#xff08;处理成String类型&#xff09; //处理请求参数的两种方法&#xff1a; //方法一&#xff1a; //获取请求 Arguments args sampler.getArguments(); //转…...

前端面试:如何减少项目里面 if-else?

在前端开发中&#xff0c;大量使用 if-else 结构可能导致代码调试困难、可读性降低和冗长的逻辑。不妨考虑以下多种策略来减少项目中的 if-else 语句&#xff0c;提高代码的可维护性和可读性&#xff1a; 1. 使用对象字面量替代 用对象字面量来替代 if-else 语句&#xff0c;…...

05.基于 TCP 的远程计算器:从协议设计到高并发实现

&#x1f4d6; 目录 &#x1f4cc; 前言&#x1f50d; 需求分析 &#x1f914; 我们需要解决哪些问题&#xff1f; &#x1f3af; 方案设计 &#x1f4a1; 服务器架构 &#x1f680; 什么是协议&#xff1f;为什么要设计协议&#xff1f; &#x1f4cc; 结构化数据的传输问题 …...

Matlab:矩阵运算篇——矩阵数学运算

目录 1.矩阵的加法运算 实例——验证加法法则 实例——矩阵求和 实例——矩阵求差 2.矩阵的乘法运算 1.数乘运算 2.乘运算 3.点乘运算 实例——矩阵乘法运算 3.矩阵的除法运算 1.左除运算 实例——验证矩阵的除法 2.右除运算 实例——矩阵的除法 ヾ(&#xffe3;…...

git reset的使用,以及解决还原后如何找回

文章目录 git reset 详解命令作用常用参数1. --soft2. --mixed&#xff08;默认参数&#xff0c;可省略&#xff09;3. --hard4. 提交引用 总结 git reset --hard HEAD^ 还原代码如何找回&#xff1f;利用 git reflog找回 git reset 详解 git reset 是 Git 中一个功能强大且较…...

react中字段响应式

class中用法: import React, { Component } from react export default class Index extends Component<any, any> { constructor(props) { super(props) this.state { settingInfo: {}, } } async componentDidMount() { let settingInfo awa…...

vue中,watch里,this为undefined的两种解决办法

提示&#xff1a;vue中&#xff0c;watch里&#xff0c;this为undefined的两种解决办法 文章目录 [TOC](文章目录) 前言一、问题二、方法1——使用function函数代替箭头函数()>{}三、方法2——使用that总结 前言 ‌‌‌‌‌尽量使用方法1——使用function函数代替箭头函数()…...

智能客服意图识别:结合知识库数据构建训练语料的专业流程

智能客服意图识别&#xff1a;结合知识库数据构建训练语料的专业流程 构建基于知识库的智能客服意图识别模型&#xff0c;需要综合运用 NLP&#xff08;自然语言处理&#xff09;、知识图谱、机器学习 等技术&#xff0c;确保意图识别的准确性和覆盖度。以下是专业的流程&…...

Spring Boot集成Spring Statemachine

Spring Statemachine 是 Spring 框架下的一个模块&#xff0c;用于简化状态机的创建和管理&#xff0c;它允许开发者使用 Spring 的特性&#xff08;如依赖注入、AOP 等&#xff09;来构建复杂的状态机应用。以下是关于 Spring Statemachine 的详细介绍&#xff1a; 主要特性 …...

压缩空气储能仿真simulink模型

压缩空气储能仿真simulink模型&#xff0c;适合matlab 2017及以上版本 CompressingGas.slx , 40474...

Tomcat 安装

一、Tomcat 下载 官网&#xff1a;Apache Tomcat - Welcome! 1.1.下载安装包 下载安装包&#xff1a; wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.102/bin/apache-tomcat-9.0.102.tar.gz 安装 javajdk。 yum install java-1.8.0-openjdk.x86_64 -y /etc/altern…...

贪心算法和遗传算法优劣对比——c#

项目背景&#xff1a;某钢管厂的钢筋原材料为 55米&#xff0c;工作需要需切割 40 米&#xff08;1段&#xff09;、11 米&#xff08;15 段&#xff09;等 4 种规格 &#xff0c;现用贪心算法和遗传算法两种算法进行计算&#xff1a; 第一局&#xff1a;{ 40, 1 }, { 11, 15…...

系统开发资源

一、前端篇 1.1 菜鸟CSS教程 1.2 HTML/CSS/JS 在线工具 二、后端篇 三、其他篇 3.1 菜鸟官网 3.2 黑马程序员学习路线 3.3 根据地区获取经纬度...

深度学习 bert与Transformer的区别联系

BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;和Transformer都是现代自然语言处理&#xff08;NLP&#xff09;中的重要概念&#xff0c;但它们代表不同的层面。理解这两者之间的区别与联系有助于更好地掌握它们在NLP任务中的应用。 …...

unity使用mesh 画图(1)

plane 圆 空心椭圆 椭圆 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI;public class DrawMeshManager {static DrawMeshManager instance;public static DrawMeshManager Instance {get {if (instance ! null){retu…...

SpringMVC响应页面及不同类型的数据,

目录 响应页面 响应数据 文本数据 响应POJO对象 ​编辑 响应生命周期 视图解析器 控制器&#xff08;Controller&#xff09;处理完客户端请求后&#xff0c;生成的并返回给客户端的结果就是响应&#xff0c;响应的结果可以是静态页面&#xff0c;数据&#xff0c;HTM…...

地理信息系统(ArcGIS)在水文水资源及水环境中的应用:空间数据管理‌、空间分析功能‌、‌可视化表达‌

随着全球工业化和经济的快速发展&#xff0c;水资源短缺、水污染等问题日益严峻&#xff0c;成为制约可持续发展的重大瓶颈。地理信息系统&#xff08;GIS&#xff09;以其强大的空间数据管理和分析能力&#xff0c;在水文水资源及水环境的研究和管理中展现出独特优势。本文将深…...

电路原理(电容 集成电路NE555)

电容 1.特性&#xff1a;充放电&#xff0c;隔直流&#xff0c;通交流 2.电容是通过聚集正负电荷来存储电能的 3.电容充放电过程可等效为导通回路 4.多电容并联可以把容量叠加&#xff0c;但是多电容串联就不会&#xff0c;只会叠加电容的耐压值。 6.电容充放电时相当于通路&a…...

C++对象的初始化和对象所占资源的清理-----初始化列表

一、初始化列表 C 提供了 初始化列表&#xff08;initializer list&#xff09; 语法&#xff0c;可以在 构造函数 中用来初始化类的成员变量。它的主要优势是 提高效率&#xff0c;特别是在初始化 const 或 reference 类型的成员时&#xff0c;以及避免额外的赋值操作。 1.…...

零成本搭建Calibre个人数字图书馆支持EPUB MOBI格式远程直读

文章目录 前言1.网络书库软件下载安装2.网络书库服务器设置3.内网穿透工具设置4.公网使用kindle访问内网私人书库 前言 嘿&#xff0c;各位书虫们&#xff01;今天要给大家安利一个超级炫酷的技能——如何在本地Windows电脑上搭建自己的私人云端书库。亚马逊服务停了&#xff…...

Ansible命令行模式常用模块使用案例(二)

在Ansible中&#xff0c;命令行模式&#xff08;Ad-Hoc 模式&#xff09;是一种快速执行任务的方式&#xff0c;适合临时任务或简单操作。以下是 Ansible 命令行模式中常用模块的使用案例&#xff08;第二部分&#xff09;&#xff1a; 1 file模块 功能特性&#xff1a;主要用于…...

12. Pandas :使用pandas读Excel文件的常用方法

一 read_excel 函数 其他参数根据实际需要进行查找。 1.接受一个工作表 在 11 案例用到的 Excel 工作簿中&#xff0c;数据是从第一张工作表的 A1 单元格开始的。但在实际场景中&#xff0c; Excel 文件可能并没有这么规整。所以 panda 提供了一些参数来优化读取过程。 比如 s…...

Pytorch中矩阵乘法使用及案例

六种矩阵乘法 torch中包含许多矩阵乘法&#xff0c;大致可以分为以下几种&#xff1a; *&#xff1a;即a * b 按位相乘&#xff0c;要求a和b的形状必须一致&#xff0c;支持广播操作 torch.matmul()&#xff1a;最广泛的矩阵乘法 &#xff1a;与torch.matmul()效果一样&…...

【MySQL】增删改查进阶

目录 一、数据库约束 约束类型 NULL约束&#xff1a;非空约束 UNIQUE&#xff1a;唯一约束 DEFAULT&#xff1a;默认值约束 PRIMARY KEY&#xff1a;主键约束 FOREIGN KEY&#xff1a;外键约束 二、表的设计 三、新增 四、查询 聚合查询 聚合函数 GROUP BY子句 HA…...

【Linux 指北】常用 Linux 指令汇总

第一章、常用基本指令 # 注意&#xff1a; # #表示管理员 # $表示普通用户 [rootlocalhost Practice]# 说明此处表示管理员01. ls 指令 语法&#xff1a; ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xf…...

父组件中循环生成多个子组件时,有且只有最后一个子组件的watch对象生效问题及解决办法

提示&#xff1a;父组件中循环生成多个子组件时&#xff0c;有且只有最后一个子组件的watch对象生效问题及解决办法 文章目录 [TOC](文章目录) 前言一、问题二、解决方法——使用function函数代替箭头函数()>{}总结 前言 ‌‌‌‌‌问题&#xff1a;子组件用that解决watch无…...

stable Diffusion 中的 VAE是什么

在Stable Diffusion中&#xff0c;VAE&#xff08;Variational Autoencoder&#xff0c;变分自编码器&#xff09;是一个关键组件&#xff0c;用于生成高质量的图像。它通过将输入图像编码到潜在空间&#xff08;latent space&#xff09;&#xff0c;并在该空间中进行操作&…...

麒麟v10 ARM64架构系统升级mysql数据库从mysql-5.7.27到mysql-8.4.4图文教程

1、背景与问题说明 因mysql-5.2.27版本存在安全漏洞问题&#xff0c;为保障系统安全&#xff0c;需将处于生产环境的麒麟v10 ARM64架构系统服务器上当前部署的mysql-5.7.27版本升级到mysql-8.4.4&#xff0c;以规避潜在风险&#xff0c;提升系统整体的安全性和稳定性。 1.1 本…...

图论·拓扑排序

拓扑排序 有向无环图的遍历 检查有向图是否连通/有环 核心操作 统计度数&#xff0c;对于度为0的点作为起始点&#xff0c;添加度为0的点作为遍历 如何验证有环&#xff1f;注意不建议直接模拟&#xff0c;如果出现环这起始点的度一定不为0&#xff0c;肯定会少遍历一些点&…...

Uniapp组件 Textarea 字数统计和限制

Uniapp Textarea 字数统计和限制 在 Uniapp 中&#xff0c;可以通过监听 textarea 的 input 事件来实现字数统计功能。以下是一个简单的示例&#xff0c;展示如何在 textarea 的右下角显示输入的字符数。 示例代码 首先&#xff0c;在模板中定义一个 textarea 元素&#xff…...

一文了解JVM的垃圾回收

Java堆内存结构 java堆内存是垃圾回收器管理的主要区域&#xff0c;也被称为GC堆。 为了方便垃圾回收&#xff0c;堆内存被分为新生代、老年代和永久代。 新创建的对象的内存会在新生代中分配&#xff0c;达到一定存活时长后会移入老年代&#xff0c;而永久代存储的是类的元数…...

Vector底层结构和源码分析(JDK1.8)

参考视频&#xff1a;韩顺平Java集合 Vector 类的定义说明&#xff1a; Vector 的底层也是一个对象数组&#xff0c;protected Object[] elementData;Vector 是线程同步的&#xff0c;即线程安全&#xff0c;Vectoe 类的操作方法带有 synchronized 关键字&#xff1a;public sy…...

uni-app+vue3学习随笔

目录相关 static文件 编译器会把static目录中的内容整体复制到最终编译包内&#xff0c; 非 static 目录下的文件&#xff08;vue组件、js、css 等&#xff09;只有被引用时&#xff0c;才会被打包编译。 css、less/scss 等资源不要放在 static 目录下&#xff0c;建议这些…...

JetBrains(全家桶: IDEA、WebStorm、GoLand、PyCharm) 2024.3+ 2025 版免费体验方案

JetBrains&#xff08;全家桶: IDEA、WebStorm、GoLand、PyCharm&#xff09; 2024.3 2025 版免费体验方案 前言 JetBrains IDE 是许多开发者的主力工具&#xff0c;但从 2024.02 版本起&#xff0c;JetBrains 调整了试用政策&#xff0c;新用户不再享有默认的 30 天免费试用…...

移远通信联合德壹发布全球首款搭载端侧大模型的AI具身理疗机器人

在汹涌澎湃的人工智能浪潮中&#xff0c;具身智能正从实验室构想迈向现实应用。移远通信凭借突破性的端侧AI整体解决方案&#xff0c;为AI机器人强势赋能&#xff0c;助力其实现跨行业拓展&#xff0c;从工业制造到服务接待&#xff0c;再到医疗康养&#xff0c;不断改写各行业…...

嵌入式硬件篇---手柄控制控制麦克纳姆轮子

文章目录 前言1. 变量定义2. 摇杆死区设置3. 模式检查4. 摇杆数据处理4.1 右摇杆垂直值&#xff08;psx_buf[7]&#xff09;4.2 右摇杆水平值&#xff08;psx_buf[8]&#xff09;4.3 左摇杆水平值&#xff08;psx_buf[5]&#xff09;4.4 左摇杆垂直值&#xff08;psx_buf[6]&am…...

XML Schema 实例

XML Schema 实例 引言 XML(可扩展标记语言)是一种用于标记电子文件使其具有结构性的标记语言。XML Schema 是一种用于定义 XML 文档结构的机制,它定义了 XML 文档中允许的数据类型、元素和属性。本文将详细探讨 XML Schema 实例,包括其基本概念、结构、用途以及实例分析。…...

Datax-web部署文档(超详细)

Datax-web部署文档&#xff08;超详细&#xff09; Datax部署 # 参考官方文档 https://github.com/alibaba/DataX/blob/master/userGuid.md# 下载datax已经封装好的文件&#xff0c;不推荐源码自己编译 https://datax-opensource.oss-cn-hangzhou.aliyuncs.com/202309/datax.…...

基于javaweb的SSM敬老院养老院管理系统(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论…...