C# 事件使用详解

发布于:2025-03-14 ⋅ 阅读:(22) ⋅ 点赞:(0)

总目录


前言

在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
{
    // 使用泛型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)允许我们在订阅或取消订阅事件时执行自定义逻辑。通过重写 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文档 - 事件


网站公告

今日签到

点亮在社区的每一天
去签到