.NET 如何有效侦听引发的事件?

How does .NET efficiently listen for raised events?

它是否使用某种轮询事件队列的事件线程?此外,该技术是否因事件类型而异?有些事件是由程序本身引发的,例如单击按钮,而其他事件是由外部引发的,例如 FileSystemWatcher 读取的 FileCreated 事件。这些事件在幕后的处理方式不同吗?

TL;DR: Event listeners do not actually have to actively listen; they get called back by the event-triggering party. See the Observer pattern.

就像属性一样,实际上只不过是一组方法和添加的一些语法糖,.NET 事件也不过是其他东西(多播委托)和一些语法糖。

就像属性一样,您可以通过语言自动实现它们(例如 string Name { get; set; }),事件通常也以非常相似的方式 "auto-implemented"(除非您专门实现一个事件你自己)。如果你想自己实现一个事件(这是非常罕见的事情),它可能看起来像这样(简化):

public event Action Completed
{
    add  // gets called for each `obj.Completed += value;`
    {
        if (completed == null)
        {
            completed = new Action(value);
        }
        else
        {
            completed += value;
        }
    }
    remove  // gets called for each `obj.Completed -= value;`
    {
        if (completed != null)
        {
            completed -= value;
        }
    }
}

private Action completed;  // backing field (a delegate) for the event

与大多数数据属性一样,每个事件通常也有一个支持字段——即多播委托。订阅事件 (Completed += …) 或取消订阅事件 (-=) 会转换为对 addremove 访问器方法的调用。

(多播)委托有一个内部方法 invocation list。您可以通过 += 运算符添加一个方法(如上面 add 访问器中发生的那样)或通过 -= 运算符将其从调用列表中删除(如上面的 remove访问器)。所以请注意 +=-= 做不同的事情取决于它们是应用于事件(调用 addremove)还是委托(add/remove 通过对 Delegate.CombineDelegate.Remove).

的后台调用从内部调用列表中获取方法

事件订阅者不必轮询;当事件被触发时,他们将被调用。无论哪一方 raises/triggers 事件实际上只是调用 "backing-field" 委托;调用该委托意味着调用委托调用列表中的每个方法——即订阅者的事件处理程序方法。

这是一个非常广泛的话题,我只能合理地涵盖基础知识。这些机制并不特定于 .NET,它们适用于在 Windows 上运行的任何程序。操作系统或其他程序可以通过两种基本方式触发事件。

第一个是您假设的,按钮的 Click 事件以及几乎所有与 GUI 程序关联的事件的底层机制。核心 .NET 调用是 Application.Run(),它启动一个调度程序循环。也称为 "pumping the message loop"。一般的解决方案为producer-consumer problem。生成事件的基本 winapi 函数是 SendMessage() 和 PostMessage()。 .NET 程序具有将这些消息转换为您可以订阅的事件的管道,NativeWindow class 就是一个很好的例子。它的 WndProc() 方法在收到消息时运行。然后它可以根据特定消息引发特定事件。

第二个是操作系统可以在任意工作线程上对函数进行回调,通常是从线程池中提取的线程。 FileSystemWatcher 就是其中的一个示例,设置它的底层 winapi 函数是 ReadDirectoryChangesW()。它支持重叠 I/O,允许它异步操作。换句话说,您可以要求它立即开始工作并return。操作系统然后在作业完成时发出事件信号或进行回调。与第一种机制不同,这类事件的工作方式隐含在任意线程上触发。

了解更多有关 winapi 的信息是理解所有这些的必要条件。

活动使用 代表。让我们先看看委托,然后看看它们是如何被事件使用的。

代表

A delegate 有点像托管函数指针。您可以创建一个委托,在调用时, 调用它指向的函数。委托经过类型检查,因此它们的参数类型和 return 类型必须与您要调用的函数匹配。您通过 委托类型 指定参数和 return 类型。例如,我在这里为 return 一个 string 并且不带参数的函数定义了一个委托类型:

delegate string MyDelegate();

现在我可以实例化 MyDelegate 以使其指向我想要的任何函数。例如,我创建了一个指向 Do 静态函数的新委托 d,并调用它。您可以自己尝试一下:

class Program {

    delegate string MyDelegate();

    static string Do() {
        return "DO!";
    }

    static void Main() {
        MyDelegate d = new MyDelegate(Do);

        Console.WriteLine(d());     // Prints: DO!
    }
}

.NET 框架有一些您可能熟悉的内置委托类型,例如 Action<...> and Func<TResult, ...> 委托系列。

好了,现在我们对委托有了基本的了解。让我们看看它们是如何在事件中使用的。

活动

你通常这样定义一个新事件:

event EventHandler Click;

这里,EventHandler 是预定义的委托类型,签名如下:

public delegate void EventHandler(Object sender, EventArgs e)

请注意,委托与您为处理事件而编写的事件处理程序完全对应:

void HandleButtonClick(Object sender, EventArgs e) {
    // Do something!
}

当您使用 += 运算符将事件处理程序 HandleButtonClick 注册到 Click 事件时,它会将指向您的函数的委托添加到事件的 multi -cast delegate.

this.Click += HandleButtonClick;

多播委托就像常规委托一样,但它可以调用多个函数,一个接一个,顺序不限。

当您使用事件时,您实际上是在调用委托来调用所有这些函数:

this.Click();

现在你知道事件是如何运作的了:委托。