>> >> >> Reference << << << <<<<<<Ref>>>>>>
Event
Modified: 2025-06-01 | Author:ljf12825

事件是C#和.NET框架中实现发布-订阅模式的核心机制。它允许一个对象(发布者)在特定事情发生时,通知其他多个对象(订阅者)。这种设计实现了对象之间的松耦合,发布者不需要知道谁订阅了它,也不需要知道订阅者将如何处理通知

核心概念

事件的五个组成部分

  1. 委托:事件的契约蓝图。它定义了订阅者的事件处理方法必须具有的签名(返回值类型和参数列表)
  2. 事件:使用event关键字声明的对象,它是委托的一个封装后的、安全的“包装器”
  3. 事件数据:一个从EventArgs派生的类,用于在触发事件时向订阅者传递相关信息
  4. 事件触发者:发布者类中负责调用事件(即调用封装在事件内的委托)的代码
  5. 事件处理器:订阅者类中符合委托签名的方法,用于响应事件

声明和使用事件的完整步骤

以“按钮点击”为例

  1. 定义事件数据类 通常继承自EventArgs。如果不需要传递额外数据,可以直接使用EventArgs.Empty
// 自定义事件数据类,用于传递点击发生的时间
public class ButtonClickedEventArgs : EvnetArgs
{
    public DateTime ClickedTime { get; }

    public ButtonClickedEventArgs(DateTime time) => ClickedTime = time;
}
  1. 定义委托 .NET提供了一个通用的委托类型EventHandler<TEventArgs>,在大多数情况下不需要自定义委托
// 过去可能需要这样定义委托
// public delegate void ButtonClickEventHandler(object sender, ButtonClickedEventArgs e);

// 现在,直接使用 EvnetHandler<T>即可
// 它的签名是:void (object sender, TEventArgs e)
  1. 在发布者类中声明事件 使用event关键字,后跟委托类型和事件名称
// 发布者
public class Button
{
    // 1. 声明事件
    public event EventHandler<ButtonClickedEventArgs> Clicked;

    // 一个模拟按钮被按下去的方法
    public void SimulateClick()
    {
        Console.WriteLine("按钮被按下了...");
        OnClicked(new ButtonClickedEventArgs(DateTime.Now));
    }

    // 2. 定义触发事件的方法(约定以 On 开头)
    // 使用 virtual 关键字允许子类重写触发逻辑
    protected virtual void OnClicked(ButtonClickedEventArgs e)
    {
        // 临时将事件赋值给一个局部变量,防止竞态条件(在检查null后,另一个线程取消订阅)
        // EventHandler<ButtonClickedEventArgs> handler = Clicked;

        // 检查是否有订阅者
        // if (handler != null)
        // {
        //     // 触发事件!‘this’是发送者(发布者自己),e是事件数据
        //     handler(this, e);
        // }

        // 在C# 6.0 及以后,可以使用更简洁的Null条件操作符
        Clicked?.Invoke(this, e);
    }
}
  1. 在订阅者类中创建事件处理器并订阅 事件处理器就是一个与委托签名匹配的方法
// 订阅者
public class UserInterface
{
    public UserInterface(Button button)
    {
        // 订阅事件:使用+=操作符
        button.Clicked += OnButtonClicked;

        // 也可以使用 Lambda 表达式
        // button.Clicked += (sender, e) => Console.WriteLine($"Lambda: 按钮在 {e.ClickedTime}被点击了");
    }

    // 事件处理器方法
    private void OnButtonClicked(object sender, ButtonClickedEventArgs e)
    {
        // 可以获取是哪个按钮触发的
        // Button button = sender as Button; 
        Console.WriteLine($"用户界面收到通知:按钮在{e.ClickedTime}被点击了。");
        // 这里可以更新UI,比如改变按钮颜色、弹出对话框等
    }

    // 一个用于取消订阅的方法
    public void UnsubscribeFromButton(Button button)
    {
        button.Clicked -= OnButtonClicked;
    }
}
  1. 运行程序
class Program
{
    static void Main(string[] args)
    {
        // 1. 创建发布者
        Button myButton = new Button();

        // 2. 创建订阅者,并订阅事件
        UserInterface ui = new UserInterface(myButton);

        // 3. 模拟事件发生
        myButton.SimulateClick();

        // 4. 取消订阅(如果需要)
        // ui.UnsubscribeFromButton(myButton);

        Console.ReadKey();
    }
}

输出

按钮被按下了...
用户界面收到通知:按钮在 YYYY-mm-dd HH:MM:SS 被点击了

EventHandler

EventHandler是.NET框架中预定义的一个委托类型,专门用于表示不携带自定义数据的事件处理方法
它定义在System命名空间中

public delegate void EventHandler(object sender, EventArgs e);

存在意义:统一的事件模式

在早期.NET中,如果没有标准化委托,每个事件都需要自定义委托

// 不统一的做法-每个事件都有自己的委托
public delegate void ClickHandler(object sender);
public delegate void LoadHandler(object sender, string message);
public delegate void CloseHandler();

public event ClickHandler Clicked;
public event LoadHandler Loaded;
public event CloseHandler Closed;

这导致了:

EventHandler引入了统一的标准

// 统一的做法 - 所有事件都使用 EventHandler
public event EventHandler Clicked;
public event EventHandler Loaded;
public event EventHandler Closed;

基本用法

声明事件

public class Button
{
    // 声明使用 EventHandler 委托的事件
    public event EventHandler Clicked;

    public void SimulateClick()
    {
        Console.WriteLine("按钮被点击...");
        OnClicked();
    }

    protected virtual void OnClicked()
    {
        // 触发事件,使用 EventArgs.Empty 表示没有额外数据
        Clicked?.Invoke(this, EventArgs.Empty);
    }
}

订阅和处理事件

public class UserInterface
{
    public UserInterface(Button button)
    {
        // 订阅事件
        button.Clicked += OnButtonClicked;

        // 或者使用 Lambda表达式
        button.Clicked += (sender, e) =>
        {
            Console.WriteLine("Lambda:按钮被点击了!");
        };
    }

    // 事件处理方法 - 必须匹配 EventHandler 签名
    private void OnButtonClicked(object sender, EventArgs e)
    {
        Console.WriteLine("收到按钮点击事件");

        // 可以通过 sender 参数知道哪个对象触发的事件
        Button clickedButton = sender as Button;
        if (clickedButton != null)
        {
            Console.WriteLine($"触发者:{clickedButton.GetType().Name}");
        }
    }
}

使用

class Program
{
    static void Main()
    {
        var button = new Button();
        var ui = new UserInterface(button);

        button.SimulateClicked();
    }
}

EventHandler vs EventHandler<T>

特性EventHandlerEventHandler<TEventArgs>
定义delegate void EventHandler(object sender, EventArgs e)delegate void EventHandler<T>(object sender, T e)
数据传递只能使用EventArgs.Empty可以传递自定义事件数据
使用场景不需要额外数据的事件需要传递数据的事件
示例event EventHandler Clickedevent EventHandler<MouseEventArgs> MouseClick
public event EventHandler Initialized; // 初始化完成
public event EventHandler Shutdown; // 关闭
public event EventHandler StatusChanged; // 状态改变
public event EventHandler<PriceChangedEventArgs> PriceChanged; // 价格变化
public event EventHandler<ErrorEventArgs> ErrorOccurred; // 错误发生
public event EventHandler<ProgressEventArgs> ProgressUpdated; // 进度更新

EventArgs

EventArgs是.NET类库中的一个类,位于System命名空间。它的核心作用是作为所有事件数据类的基类

// 在.NET中的定义(简化)
public class EventArgs
{
    public static readonly EventArgs Empty;
    public EventArgs();
}

它本身不包含任何数据。它主要提供两种用途

  1. 作为一个标记:表示这是一个用于事件参数的类型
  2. 作为基类:可以派生出自定义的事件数据类

存在意义

在没有EventArgs的情况下,如果想通过事件传递数据,可能会这样定义委托和事件

// 不推荐的做法:为每种数据都定义不同的委托
public delegate void ClickEventHandler(int x, int y, string buttonName);
public event ClickEventHandler Clicked;

这种做法的问题:

EventArgs通过引入一个标准化的、可扩展的容器来解决这些问题

使用场景

1. 不需要传递任何数据

当事件本身的发生就是唯一的信息时(例如,“任务完成”、“状态重置”),可以使用EventArgs.Empty。这是一个静态只读字段,表示一个空的、不包含任何数据的事件参数实例

public class Timer
{
    // 使用 EventHandler 而不是 EventHandler<T>,因为不需要自定义数据
    public event EventHandler Tick;

    protected virtual void OnTick()
    {
        // 使用 EventArgs.Empty
        Tick?.Invoke(this, EventArgs.Empty)
    }
}

// 订阅
timer.Tick += (sender, e) =>
{
    // 这里的e就是EventArgs.Empty
    Console.WriteLine("Tick! 无需任何额外数据");
}

2. 需要传递自定义数据

这是EventArgs最主要的使用场景。需要创建一个继承自EventArgs的类

// 自定义事件数据类
public class PriceChangedEventArgs : EventArgs
{
    // 通常属性是只读的,以保证在事件处理过程中的数据一致性
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }

    public PriceChangedEventArgs(decimal oldPrice, decimal newPrice)
    {
        OldPrice = oldPrice;
        NewPrice = newPrice;
    }
}

// 在发布者类中使用
public class Stock
{
    private decimal _price;

    // 声明使用自定义 EventArgs 的事件
    public event EventHandler<PriceChangedEventArgs> PriceChanged;

    public decimal Price
    {
        get => _price;
        set
        {
            if (_price == value) return; // 价格未变,直接返回

            decimal oldPrice = _price;
            _price = value;

            // 价格变化时,触发事件并传递旧价格和新价格
            OnPriceChanged(new PriceChangedEventArgs(oldPrice, _price));
        }
    }

    protected virtual void OnPriceChanged(PriceChangedEventArgs e)
    {
        PriceChanged?.Invoke(this, e);
    }
}

// 订阅者
var stock = new Stock();
stock.PriceChanged += (sender, e) => 
{
    Console.WriteLine($"价格从 {e.OldPrice:C} 变为 {e.NewPrice:C}"); // e 是 PriceChangedEventArgs 类型
    // 可以根据新旧价格做出不同反应,例如:
    if (e.NewPrice > e.OldPrice)
    {
        Console.WriteLine("股票上涨了!");
    }
};

设计EventArgs类的最佳实践

  1. 命名:类名应以EventArgs结尾,例如MouseEventArgs, KeyPressEventArgs
  2. 不可变性:事件数据对象在创建后不应该被修改。因此,通常:
    • 通过构造函数来初始化所有数据
    • 只提供只读属性(只有get访问器)来暴露数据
public class MailReceivedEventArgs : EventArgs
{
    public string From { get; }
    public string Subject { get; }

    public MailReceivedEventArgs(string from, string subject)
    {
        From = from;
        Subject = subject;
    }
}
  1. 继承自 EventArgs:始终从EventArgs派生,这是.NET的通用约定
  2. 包含相关信息:只包含订阅者处理该事件时真正需要的数据。不要传递整个发布者对象,而是通过sender参数来访问发布者

sender参数

在事件处理器中,除了EventArgs e,还有一个object sender参数。它是对触发事件的发布者对象的引用

stock.PriceChanged += (sender, e) =>
{
    // 可以将 sender 转换回具体的类型,以访问其成员
    Stock stockWhichChanged = sender as Stock;
    if (stockWhichChanged != null)
    {
        Console.WriteLine($"当前股票价格是:{stockWhichChanged.Price}");
    }
    
    // 同时使用 sender 和 e
    Console.WriteLine($"变化详情:{e.OldPrice} -> {e.NewPrice}");
};

使用建议:

?.Invoke()

?.Invoke()主要用于安全地调用委托和事件,它实际上是两个操作的组合:

组合在一起,它的作用是:如果左边的对象不为null,就调用其Invoke方法;如果为null,就什么都不做,返回null

存在意义

传统方式的问题
在C# 6.0 引入?.操作符之前,这样触发事件

public class Button
{
    public event EventHandler Clicked;

    protected virtual void OnClicked()
    {
        // 传统方式 - 存在竞态条件风险
        if (Clicked != null)
        {
            Clicked(this, EventArgs.Empty);
        }
    }
}

问题:在多线程环境下,可能存在竞态条件

  1. 线程A检查Clicked != null -> 结果为 true
  2. 线程B取消了事件订阅,设置Clicked = null
  3. 线程A执行Clicked(this, EventArgs.Empty) -> 抛出NullReferenceException

解决方案1:使用局部变量(C# 6.0之前)

protected virtual void OnClicked()
{
    // 将委托复制到局部变量
    EventHandler handler = Clicked;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

原理:委托是不可变的,+=-=操作实际上回创建新的委托实例。将事件复制到局部变量后,即使其他线程修改了原始事件,局部变量仍然指向原来的委托链

解决方案2:使用?.Invoke()(C# 6.0+)

protected virtual void OnClicked()
{
    // 现代方式 - 线程安全且简洁
    Clicked?.Invoke(this, EventArgs.Empty);
}

这行代码等价于

var handler = Clicked;
if (handler != null)
{
    handler.Invoke(this, EventArgs.Empty);
}

工作原理

// 这行代码:
Clicked?.Invoke(this, EvnetArgs.Empty);

// 实际上被编译器转换为
var temp = Clicked;
if (temp != null)
{
    temp.Invoke(this, EventArgs.Empty);
}

关键点:

  1. 线程安全:先将事件引用复制到临时变量
  2. 空值检查:检查临时变量是否为null
  3. 安全调用:只有不为null时才调用Invoke

为什么使用事件而不是简单的委托

事件本质上是委托的安全封装

示例对比

public class PublisherWithDelegate
{
    public Action PublicDelegate; // 公共委托
    public event Action PublicEvent; // 公共事件

    public void Test()
    {
        // 在类内部,两者都可以调用
        PublicDelegate?.Invoke();
        PublicEvent?.Invoke();
    }
}

// 在另一个类中
var pub = new PublisherWithDelegate()

// 委托的危险操作
pub.PublicDelegate = () => Console.WriteLine("Handler 1"); // 直接赋值,清空其他订阅
pub.PublicDelegate += () => Console.WriteLine("Handler 2");
pub.PublicDelegate(); // 外部可以直接触发!这可能不是我们想要的

// 事件的安全操作
// pub.PublicEvent = ... // 错误!编译不通过,不能直接赋值
pub.PublicEvent += () => Console.WriteLine("EventHandler 1");
pub.PublicEvent += () => Console.WriteLine("EventHandler 2");
// pub.PublicEvent(); // 错误!编译不通过,外部不能触发事件

event实际上是编译器在委托基础上生成了一组add/remove访问器

public event EventHandler Clicked;
// 编译后大致等于
private EventHandler _Clicked;
public void add_Clicked(EventHandler value) => _Clicked += value;
public void remove_Clicked(EventHandler value) => _Clicked -= value;

现代语法与高级技巧

Lambda表达式订阅

传统订阅事件时,必须写完整方法

button.Clicked += OnButtonClicked;

private void OnButtonClicked(object sender, EventArgs e)
{
    Console.WriteLine("按钮被点击");
}

现代C#提供Lambda表达式,直接内联事件处理逻辑

button.Clicked += (_, e) => Console.WriteLine("Lambda: 按钮被点击!");

特点:

button.Clicked -= (_, e) => Console.WriteLine("..."); // ❌ 无法解除,会生成不同的匿名类

如果可能需要取消订阅,应保留委托引用

EventHandler handler = (_, e) => Console.WriteLine("Lambda:按钮被点击!");
button.Clicked += handler;
button.Clicked -= handler; // 可以正确取消

轻量事件:使用ActionFunc替代标准模式

当在引擎、工具或内部系统中实现简化事件机制时,不一定非要用EventHandler
C#允许使用任意委托类型作为事件

public event Action<int> HealthChanged; // 比 EventHandler<T> 更轻

protected void OnHealthChanged(int hp)
{
    HealthChanged?.Invoke(hp);
}

优点:

缺点:

线程安全触发:?.Invoke()的底层原理

C# 6 引入的Null条件操作符简化了事件触发

Clicked?.Invoke(this, EventArgs.Empty);

编译器会自动生成线程安全代码

var temp = Clicked;
if (temp != null)
    temp.Invoke(this, EventArgs.Empty);

核心点:

弱事件(WeakEventManager)

强引用事件的风险:
订阅者(Subscriber)被发布者(Publisher)强引用,若发布者生命周期更长,则订阅者无法被GC释放,造成内存泄露

解决方案:弱事件

using System.Windows;

WeakEventManager<Button, EventArgs>.AddHandler(button, "Clicked", OnButtonClicked);
WeakEventManager<Button, EventArgs>.RemoveHandler(button, "Clicked", OnButtonClicked);

适用场景:

原理:

反射访问事件(高级元编程)

可以通过反射读取类型中声明的所有事件

var events = typeof(Button).GetEvents();
foreach (var e in events)
    Console.WriteLine($"事件名:{e.Name},委托类型:{e.EventHandlerType}");

可以用于

不过

异步事件:事件 + async/await

事件处理器可以是异步方法

public event Func<object, EventArgs, Task> DataLoaded;

protected virtual async Task OnDataLoadedAsync()
{
    if (DataLoaded != null)
        await DataLoaded.Invoke(this, EventArgs.Empty);
}

这允许订阅者异步执行逻辑(例如网络请求、IO操作)而不阻塞主线程
但要注意:

最佳实践和注意事项

  1. 命名约定:
    • 事件名使用PasalCase,如Clicked,ValueChanged
    • 事件处理器方法名通常为On+事件名,如OnButtonClicked
    • 触发事件的方法名为On+事件名,如OnClicked
  2. 线程安全:始终使用?.Invoke()或局部变量来触发事件,以防止竞态条件
  3. 及时取消订阅:如果订阅者的生命周期短于发布者,务必在订阅者销毁前取消订阅(例如在Dispose方法中)。否则,发布者会持有对订阅者的引用,导致内存泄露,因为垃圾回收器无法回收仍在被引用的对象
  4. 事件数据不可变性:EventArgs派生类中的属性应该是只读的,以防止订阅者在事件处理过程中修改数据,影响其他订阅者