语言

处理和引发事件

.NET 中的事件基于 委托模型。 委托模型遵循 观察者设计模式,使订阅者能够向提供程序注册和接收通知。 事件发送方在事件发生时推送通知。 事件接收器定义响应。 本文介绍委托模型的主要组件、如何在应用程序中使用事件以及如何在代码中实现事件。

使用事件发送者触发事件

事件是由对象发送的消息,用于表示动作的发生。 该作可能是用户交互(例如按钮按下),也可能是由其他程序逻辑(例如属性值更改)导致的。 引发事件的对象称为 事件发送方。 事件发送方不知道接收或处理它引发的事件的对象或方法。 事件通常是事件发送方的成员。 例如,该 Elapsed 事件是类的成员 Timer ,事件 PropertyChanged 是实现 INotifyPropertyChanged 接口的类的成员。

若要定义事件,请在事件类的签名中使用 C# 事件 或 Visual Basic 事件 关键字,并指定事件的委托类型。 下一部分将介绍委托机制。

要触发事件,请调用与该事件关联的委托。 事件发送方通常将调用包装在一个名为protected virtual(在 Visual Basic 中为Protected Overridable)的方法中,例如On<EventName>OnDataReceived。 该方法接受一个参数:类型 EventArgs 或其派生类型的事件数据对象。 以这种方式包装调用可让派生类替代异常处理逻辑。 重写此方法的派生类必须调用基类实现,以便确保已经注册的委托对象接收事件。

以下示例演示如何声明一个名为ThresholdReached的事件。 该事件与 EventHandler 委托相关联,并在名为 OnThresholdReached 的方法中触发。

class Counter
{
    public event EventHandler? ThresholdReached;

    protected virtual void OnThresholdReached(EventArgs e)
    {
        ThresholdReached?.Invoke(this, e);
    }

    // Provide remaining implementation for the class...
}
Public Class Counter
    Public Event ThresholdReached As EventHandler

    Protected Overridable Sub OnThresholdReached(e As EventArgs)
        RaiseEvent ThresholdReached(Me, e)
    End Sub

    ' Provide remaining implementation for the class...
End Class

声明事件处理程序的委托签名

委托是保存对方法的引用的类型。 声明委托时使用签名,该签名显示其引用的方法的返回类型和参数。 它只能保存对其签名匹配的方法的引用。 委托等效于类型安全的函数指针或回调机制。 委托声明足以定义委托类。

委托在 .NET 中有许多用途。 在事件的上下文中,委托是事件源和处理事件的代码之间的中介(或类似指针的机制)。 通过在事件声明中包括委托类型,将委托与事件相关联,如上一节中的示例所示。 有关委托的详细信息,请参阅Delegate类。

.NET 提供 EventHandler 和 EventHandler<TEventArgs> 委托来支持大多数事件场景。 对于不包含事件数据的所有事件,使用 EventHandler 委托。 使用 EventHandler<TEventArgs> 委托来处理包含事件数据的事件。 这些委托没有返回类型的值,并带有两个参数(其中一个是事件源对象,另一个是事件数据对象)。

委托是 多播 类对象,这意味着它们可以保存对多个事件处理方法的引用。 有关详细信息,请参阅 Delegate 参考页。 委托在事件处理中提供灵活性和精细的控制。 委托充当引发事件的类的事件分发器,通过维护事件的已注册事件处理程序列表来实现这一功能。

使用EventHandler和EventHandler<TEventArgs>委托类型来定义所需的委托。 在声明中,您可以在delegate中使用类型,或在Delegate中使用类型标记委托。 以下示例演示如何声明一个名为ThresholdReachedEventHandler的委托:

public delegate void ThresholdReachedEventHandler(object sender, ThresholdReachedEventArgs e);
Public Delegate Sub ThresholdReachedEventHandler(sender As Object, e As ThresholdReachedEventArgs)

使用事件数据类

可以通过事件数据类提供与事件关联的数据。 .NET 提供了许多可在应用程序中使用的事件数据类。 例如,SerialDataReceivedEventArgs 类是用于 SerialPort.DataReceived 事件的事件数据类。 .NET 遵循命名模式,其中所有事件数据类以 EventArgs 后缀结尾。 您可以通过查看事件的委托来确定哪个事件数据类与该事件相关联。 例如, SerialDataReceivedEventHandler 委托包含 SerialDataReceivedEventArgs 类作为参数。

该 EventArgs 类通常是事件数据类的基类型。 如果事件没有任何与之关联的数据,也可以使用这个类。 创建一个通知订阅者某些事件发生但不包含任何附加数据的事件时,请在委托中将 EventArgs 类作为第二个参数包含进去。 如果未提供任何数据,可以传递EventArgs.Empty作为该值。 委托 EventHandler 将 EventArgs 类作为参数包含在内。

可以创建从 EventArgs 类派生的新类,以提供任何需要的成员来传递与事件相关的数据。 通常,应使用与 .NET 相同的命名模式,并使用后缀结束事件数据类名称 EventArgs 。

以下示例显示了一个名为 ThresholdReachedEventArgs 包含特定于所引发事件的属性的事件数据类:

public class ThresholdReachedEventArgs : EventArgs
{
    public int Threshold { get; set; }
    public DateTime TimeReached { get; set; }
}
Public Class ThresholdReachedEventArgs
    Inherits EventArgs

    Public Property Threshold As Integer
    Public Property TimeReached As Date
End Class

使用处理程序响应事件

若要响应事件,请在事件接收器中定义事件处理程序方法。 此方法必须与要处理的事件的委托的签名匹配。 在事件处理程序中,当事件被触发时,你需要执行必要的操作,例如在用户按下按钮后收集用户输入。 若要在事件发生时接收通知,事件处理程序方法必须订阅该事件。

以下示例显示了一个名为 c_ThresholdReached 的事件处理程序方法,该方法与 EventHandler 委托的签名匹配。 该方法订阅 ThresholdReached 事件:

class Program
{
    static void Main()
    {
        var c = new Counter();
        c.ThresholdReached += c_ThresholdReached;

        // Provide remaining implementation for the class...
    }

    static void c_ThresholdReached(object? sender, EventArgs e)
    {
        Console.WriteLine("The threshold was reached.");
    }
}
Module Program
    Sub Main()
        Dim c As New Counter()
        AddHandler c.ThresholdReached, AddressOf c_ThresholdReached

        ' Provide remaining implementation for the class...
    End Sub

    Sub c_ThresholdReached(sender As Object, e As EventArgs)
        Console.WriteLine("The threshold was reached.")
    End Sub
End Module

使用静态和动态事件处理程序

.NET 允许订阅者静态或动态注册事件通知。 静态事件处理程序对于它们处理的事件的类的整个生命周期有效。 动态事件处理程序在程序执行期间显式激活和停用,通常是为了响应某些条件程序逻辑。 当仅在特定条件下需要事件通知或运行时条件确定要调用的特定处理程序时,可以使用动态处理程序。 上一部分中的示例演示如何动态添加事件处理程序。 有关详细信息,请参阅 事件 (在 Visual Basic 中)和 事件 (在 C# 中)。

引发多个事件

如果类引发多个事件,编译器将为每个事件委托实例生成一个字段。 如果事件数量较大,每个委托使用一个字段的存储成本可能不可接受。 对于这些场景,.NET 提供了事件属性,您可以与其他所选数据结构一起使用,以存储事件委托。

事件属性由事件访问器附带的事件声明组成。 事件访问器是用于在存储数据结构中添加或删除事件委托实例的方法。

注释

事件属性比事件字段慢,因为每个事件委托必须先被检索,然后才能够调用。

权衡在于内存和速度。 如果类定义了许多不经常引发的事件,则应实现事件属性。 有关详细信息,请参阅 使用事件属性声明多个事件。

以下资源描述了与处理事件相关的其他任务和概念:

查看规格参考

规范参考文档适用于支持事件处理的 API:

API 名称 API 类型 Reference
EventHandler 委托 EventHandler
EventHandler<TEventArgs> 委托 EventHandler<TEventArgs>
EventArgs Class EventArgs
委托 Class Delegate