65.9K
CodeProject 正在变化。 阅读更多。
Home

一个易于使用的 .NET 2.0 弱引用事件处理程序工厂

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (18投票s)

2005 年 9 月 23 日

10分钟阅读

viewsIcon

156959

downloadIcon

951

一篇关于创建弱事件处理程序的文章,展示了如何在 Windows Form 中使用它们。

A picture of the sample application running

引言

从 C++/MFC 迁移到 C#/Windows Forms 的过程对我来说是一次非常积极的体验,但它并没有完全达到我的预期,即我可以随意传递对象,而无需担心在它们不再需要时删除它们。问题是,当你向事件添加事件处理程序时,事件会持有对包含该事件处理程序的对象的引用,只要该引用存在,垃圾收集器就无法收集包含该事件处理程序的对象。因此,虽然不再需要调用 delete,但我过度使用事件使我需要跟踪对象何时不再需要,以便我可以从它们正在服务的事件中删除它们的事件处理程序,从而使它们能够被垃圾回收。

在过去的几年里,我一直在为这个问题使用许多变通方法,包括改变我的编码风格以减少这个问题发生的几率,以及保留委托的弱引用列表,我会在其中迭代以模拟事件,但我最终决定坐下来编写一个易于使用的解决方案来解决这个问题,以便能够 **以我想要的方式使用事件**。

背景

我不是第一个尝试解决这个问题的人。微软的 Greg Schechter 在他的 博客 上有一篇非常鼓舞人心的文章,讨论了这个普遍存在的问题以及他提出的解决方案。虽然解决方案不错,但它并没有完全满足我的需求,因为它要求过多地更改我现有的代码库。

Interact Software Ltd. UK 的 Ian Griffiths 在他的 博客 上提出了我正在寻找的那种 解决方案,但不幸的是它并没有完全奏效。这是一个棘手的问题。

Wesner Moise 在他的博客中提到,在 2005 年 3 月与 .NET Bass Class Library 团队共进晚餐时,他讨论了弱委托,但我没有看到任何关于何时会实现此类功能的提及,所以我决定继续使用我自己的解决方案。他在同一页面上还提到,他曾见过两位专家搞砸了弱委托的实现,所以希望,尽管本文可能会受到所有可能的审查,但我没有将我的名字列入搞砸这个问题的名单。

Using the Code

根据您是否需要速度或简洁性,我有两种使用弱事件处理程序的方法。使用弱事件处理程序的简单方法是使用 WeakEventHandlerFactory 类,它要求您在类中创建 **一个** WeakEventHandlerFactory 实例

private WeakEventHandlerFactory eventHandlers;

然后,使用 AddWeakHandler 方法将任意数量的事件处理程序添加到任何事件源

eventHandlers.AddWeakHandler<MyEventArgs>(eventSource, 
              "EventSourceChanged", eventSource_EventSourceChanged);

要删除事件处理程序,请使用 RemoveWeakHandler

eventHandlers.RemoveWeakHandler<MyEventArgs>(eventSource, 
           "EventSourceChanged", eventSource_EventSourceChanged);

与通常用于添加和删除事件处理程序的代码相差不大,不是吗?应该注意的是,C# 2.0 编译器的主要新功能是将函数名转换为事件处理程序,因此 eventSource_EventSourceChanged 将被转换为 new EventHandler<MyEventArgs>(eventSource_EventSourceChanged)

当您的对象被垃圾回收时,WeakEventHandlerFactory 也会在同一时间被垃圾回收,其析构函数将删除所有尚未被删除的事件处理程序。您的对象被垃圾回收和事件处理程序工厂被垃圾回收之间存在延迟也没有关系,因为一旦您的对象被垃圾回收,事件处理程序工厂就不会尝试传递事件。

使用事件处理程序工厂的缺点是它比添加或删除普通事件处理程序慢 40 倍。在某些情况下这是不可接受的,所以我创建了一个 WeakEventHandler<> 类,它需要更多的代码来使用,但实际上与普通事件处理程序相比,添加和删除速度快 2 倍。要直接使用弱事件处理程序,需要更多的代码和谨慎。对于类中的每个事件处理程序,您需要在类声明中包含一个类似以下的实例

private WeakEventHandler<MyEventArgs> eventSourceChangedHandler;

然后在构造函数中,您以以下方式初始化 WeakEventHandler<> 对象

eventSourceChangedHandler = new 
  WeakEventHandler<MyEventArgs>(eventSource_EventSourceChanged);

这与初始化普通事件处理程序的方式类似,只不过您需要在类中保留一个 WeakEventHandler<> 的实例。要使用 WeakEventHandler<> 对象,只需像使用普通事件处理程序一样添加或删除它

eventSource.EventSourceChanged += eventSourceChangedHandler;
eventSource.EventSourceChanged -= eventSourceChangedHandler;

然而,区别在于,您 **必须** 在对象被最终确定之前删除事件处理程序,否则下次事件源触发由您对象中的 WeakEventHandler<> 处理的事件时,您的应用程序将因空引用异常而崩溃。因此,您的析构函数需要像这样删除 WeakEventHandler<>

~MyClass
{
    eventSource.EventSourceChanged -= eventSourceChangedHandler;
}

幕后

我编写了四个用于创建弱事件处理程序的实用类

  • WeakReferenceToEventHandler
  • WeakEventHandlerInternal
  • WeakEventHandlerFactory
  • WeakEventHandler

WeakReferenceToEventHandler

此类顾名思义。它是一个事件处理程序的弱引用。此类仅由 WeakEventHandlerInternal 使用,无需直接使用。

此类包含将事件处理程序添加到事件源和从事件源中删除事件处理程序的方法。所有必需的详细信息,如事件源对象和事件名称都存储在此类中,以便在原始事件处理程序被垃圾回收时,它可以从事件源中删除自身。

由于事件无法通过方法传递,因此 AddHandler 方法使用反射将中间事件处理程序添加到事件源

public void AddHandler(object eventSource, string eventName)
{
    // Store the event source details
    this.eventName = eventName;
    weakReferenceToEventSource = new WeakReference(eventSource);

    // Create and intermediate handler that the event source will
    // have a strong reference to.
    EventInfo eventInfo = eventSource.GetType().GetEvent(eventName);
    eventInfo.AddEventHandler(eventSource, 
      new EventHandler<TEventArgs>(IntermediateEventHandler));
}

类似地,RemoveHandler 方法使用反射来删除中间事件处理程序

public void RemoveHandler()
{
    if (weakReferenceToEventSource==null)
      return;

    object eventSource = weakReferenceToEventSource.Target;
    if (eventSource != null)
    {
        // Get the event using reflection
        EventInfo eventInfo = eventSource.GetType().GetEvent(eventName);

        // Remove the intermediate event handler, which will dereference
        // this weak reference and allow it to be garbage collected.
        eventInfo.RemoveEventHandler(eventSource, 
          new EventHandler<TEventArgs>(IntermediateEventHandler));
    }
}

当事件被触发时,中间事件处理程序会处理它。此时会进行测试,以确定原始事件处理程序是否已被垃圾回收,如果已被垃圾回收,则中间事件处理程序会从事件源中删除自身。

WeakEventHandlerInternal

WeakEventHandlerInternal 类用于内部存储弱事件处理程序。此类不应直接使用,因此我将其构造函数设为 internal。我有一个名为 WeakEventHandler 的类可供直接使用,其添加和删除事件的速度比 WeakEventHandlerInternal 快 8 倍。WeakEventHandlerInternal 类是对原始事件处理程序的强引用的容器,并且还包含 WeakReferenceToEventHandler 类的实例。它包含添加、删除和比较弱事件处理程序的相等性的方法。它由 WeakEventHandlerFactory 类使用。

WeakEventHandlerFactory

WeakEventHandlerFactory 是可用于创建弱事件处理程序的类。它包含两个方法

  • AddWeakHandler
  • RemoveWeakHandler

AddWeakHandler 方法创建对原始事件处理程序的弱引用,但也存储对事件处理程序的强引用,以便在离开方法后不会立即被垃圾回收。弱引用和强引用一起存储在一个添加到内部列表的对象中。

RemoveWeakHandler 在所有存储的事件处理程序中进行搜索以查找匹配项,并在找到匹配项时删除事件处理程序。此方法返回一个标志,指示是否成功。

WeakEventHandler

此类可以被直接使用,但要谨慎,用于直接创建弱事件处理程序。它以牺牲易用性为代价来提高速度。必须为每个由 WeakEventHandler 对象表示的事件处理程序保留一个 WeakEventHandler 实例,并且在处理事件的对象被垃圾回收之前,必须将每个 WeakEventHandler 对象从源事件中删除。

使用示例应用程序

如果您编译文章顶部链接的项目,您将获得一个与顶部图像所示应用程序。此应用程序演示了弱事件处理程序的实用性。

该应用程序有三个 DataGrid 控件,用于编辑最多三个集合的内容。在每个 DataGrid 的上方,是正在编辑的集合的唯一标识符,以及该集合中所有值的总和。

如果您编辑集合中的一个项目,DataGrid 上方的总计显示将更新。如果您删除一个项目,您将在日志窗口中看到一条消息,指示事件处理程序已成功从您从集合中删除的项目中删除。由此您可以推断,集合中的每个项目都有一个在您更改项目时触发的事件。因此,每个项目都对其父集合拥有引用,因为是集合处理事件并更新集合对象中保存的总计值。此应用程序的目的是展示使用了事件处理程序的弱引用,从而允许对象容器(集合)在不再需要时被垃圾回收,即使集合中的项目仍然存在。

现在进入有趣的部分。在每个 DataGrid 的下方有四个按钮。它们允许您显示或复制另外两个集合中的一个。

当您复制一个集合时,会创建一个新集合,并将源集合中的对象复制到其中。按下“强制垃圾回收”按钮将强制垃圾回收不再显示在任何地方的已替换集合,并且日志窗口将显示一条消息,指示哪个集合已被垃圾回收。

编辑复制集合中的项目会更新其复制的来源集合,但添加到新集合或从中删除的项目将不会影响源集合。

当您“查看一个集合”时,实际的集合本身将显示在目标 DataGrid 中,以及源 DataGrid。在一个 DataGrid 中添加/删除/编辑对象的任何操作都会在另一个 DataGrid 中镜像。

速度测试

该示例应用程序包含一个小型基准测试,用于显示不同类型的事件处理程序添加和删除的速度。我在一台旧的 3.0GHz Pentium 4 上测试了添加和删除 1,000,000 个事件处理程序,并获得了以下结果

Speed Test: Adding and Removing 1000000 Event Handlers

Adding Normally.........Seconds: 0.5781361
Removing Normally.........Seconds: 0.3906325
Adding WeakEventHandler.........Seconds: 0.156253
Removing WeakEventHandler.........Seconds: 0.2343795
Adding WeakEventHandlerInternal.........Seconds: 2.2812938
Removing WeakEventHandlerInternal.........Seconds: 1.9687878
Adding using WeakEventHandlerFactory.........Seconds: 19.7191286
Removing using WeakEventHandlerFactory.........Seconds: 17.1253288

在示例应用程序中,我将添加和删除的事件处理程序数量设置为 100,000,这样您就不必等待太长时间就能看到自己的结果。

示例应用程序中包含的示例代码

示例应用程序的源代码中包含两个集合类

  • SubTotalCollection
  • SubTotalCollectionFast

SubTotalCollection 使用 EventHandlerFactory 创建弱事件处理程序,而 SubTotalCollectionFast 直接使用 WeakEventHandler 类。SubTotalCollectionFast 是管理 WeakEventHandler 对象的绝佳示例。它在 SubTotal 对象添加到集合时添加处理程序,在 SubTotal 对象从集合中删除时删除处理程序,并且在集合被最终确定或清除时,它会从集合中仍然存在的 SubTotal 对象中删除事件处理程序。

示例应用程序仅使用 SubTotalCollection 类。要使其使用 SubTotalCollectionFast 类,只需在 Form1.cs 文件中进行搜索和替换,将 SubTotalCollection 替换为 SubTotalCollectionFast

关注点

新的 DataGrid 控件比旧的更容易使用,用于编辑简单的集合。在此示例应用程序中,我有一个 SubTotal 类型的对象集合,我希望在一个 DataGrid 中进行编辑。这需要三个步骤。第一个是声明一个继承自 BindingList 的集合类,如下所示

public class SubTotalCollection : BindingList<SubTotal>

然后在 Form.cs 中,我创建了一个绑定源,将其附加到 DataGrid,并将一个集合附加到绑定源,就这样完成了

BindingSource leftSource = new BindingSource();
dataGridViewLeft.DataSource = leftSource;
leftSource.Source = new SubTotalCollection();

在 2005 年 9 月 23 日发布的第一个版本和此版本之间,我曾多次尝试提高工厂的速度。在 RemoveHandler 方法中为查找匹配的事件处理程序添加了一个 Dictionary 后,我尝试将其更改为 SortedDictionary<,>,但对于此目的,SortedDictionary<,> 的速度似乎是使用 Dictionary<,> 类的两倍。

历史

  • 原始文章提交于 2005 年 9 月 23 日。
  • 2005 年 10 月 13 日 - 提高了速度,添加了更多注释,添加了更快的 WeakEventHandler 类。
  • 2007 年 3 月 7 日 - 修复了 Stéphane Issartel 在更快的 WeakEventHandler 类中发现的一个 bug。
© . All rights reserved.