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

C# 中的 .NET 弱事件模式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.63/5 (38投票s)

2014 年 3 月 3 日

CPOL

8分钟阅读

viewsIcon

98289

downloadIcon

122

C# 中的 .NET 弱事件模式

引言

正如您可能知道的,事件处理程序是导致内存泄漏的常见原因,因为不再使用的对象仍然存在,您可能认为它们应该已经被垃圾回收了,但实际上没有,而且有充分的理由。

在这篇(希望)简短的文章中,我将介绍 .NET Framework 中事件处理程序的问题,然后我将展示如何通过两种方式实现该问题的标准解决方案,即弱事件模式,一种是使用

  • “旧的”(好吧,在 .NET 4.5 之前,所以不算太旧)方法,这种方法实现起来相当麻烦
  • .NET 4.5 框架提供的新方法,它尽可能简单

(源代码可在此处获取。)

常见内容

在深入文章核心之前,让我们回顾一下代码中广泛使用的两个项目:一个类和一个方法。

事件源

让我向您介绍一个基本但有用的事件源类,它具有足够的复杂性来阐明这一点。

public class EventSource
{
    public event EventHandler<EventArgs> Event = delegate { };

    public void Raise()
    {
        Event(this, EventArgs.Empty);
    }
}

对于那些想知道奇怪的空委托初始化是什么意思的人:这是一个技巧,可以确保事件始终被初始化,而无需每次在使用它之前检查它是否为 `null`。

触发 GC 的实用方法

在 .NET 中,垃圾回收非确定性的方式触发,这不利于我们需要以确定性方式跟踪对象状态的测试。

所以我们必须定期自己触发 GC,并且为了避免重复编写样板代码,我们将其分解到一个专用方法中。

static void TriggerGC()
{
    Console.WriteLine("Starting GC.");

    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();

    Console.WriteLine("GC finished.");
}

这并不是什么高深的技术,但如果您不熟悉这种模式,它值得一点解释。

  • 第一个 `GC.Collect()` 触发 .NET CLR 垃圾收集器,它将负责清除未使用的对象,对于那些类没有终结器(也称为 C# 中的析构函数)的对象来说,这已经足够了。
  • `GC.WaitForPendingFinalizers()` 等待其他对象的终结器执行;我们需要它,因为正如您将看到的,我们将使用终结器方法来知道我们的对象何时被收集。
  • 第二个 `GC.Collect()` 确保新终结的对象也被清除。

问题

所以,首先,让我们尝试通过一些理论和,最重要的是,一个演示来理解事件侦听器的问题。

背景

当一个充当事件侦听器的对象将其一个实例方法注册到产生事件的对象(事件源)上的事件处理程序时,事件源必须保留对事件侦听器对象的引用,以便在该侦听器的上下文中引发事件。

这很公平,但如果这个引用是强引用,那么侦听器就充当了事件源的依赖项,即使引用它的最后一个对象是事件源,它也不能被垃圾回收。

这是一张关于幕后发生的事情的详细图表。

Events handlers issue

事件处理程序问题

如果您可以控制侦听器对象的生命周期,那么这不是问题,因为您可以在不再需要侦听器时从事件源取消订阅,通常使用可处置模式
但是,如果您无法确定侦听器生命周期的单一责任点,那么您就无法以确定性的方式处理它,并且必须依赖垃圾回收过程……只要事件源还存在,它就不会认为您的对象已准备好被回收!

演示

理论都很好,但让我们用实际代码来看看这个问题。

这是我们勇敢的事件侦听器,它有点天真,我们会很快明白为什么。

public class NaiveEventListener
{
    private void OnEvent(object source, EventArgs args)
    {
        Console.WriteLine("EventListener received event.");
    }

    public NaiveEventListener(EventSource source)
    {
        source.Event += OnEvent;
    }

    ~NaiveEventListener()
    {
        Console.WriteLine("NaiveEventListener finalized.");
    }
}

让我们看看这个实现如何在一个简单的用例中表现。

Console.WriteLine("=== Naive listener (bad) ===");

EventSource source = new EventSource();

NaiveEventListener listener = new NaiveEventListener(source);

source.Raise();

Console.WriteLine("Setting listener to null.");
listener = null;

TriggerGC();

source.Raise();

Console.WriteLine("Setting source to null.");
source = null;

TriggerGC();

输出如下

EventListener received event.
Setting listener to null.
Starting GC.
GC finished.
EventListener received event.
Setting source to null.
Starting GC.
NaiveEventListener finalized.
GC finished.

让我们分析一下工作流程:

  • EventListener received event.”:这是我们调用“source.Raise()”的结果;太好了,看起来我们正在收听。
  • Setting listener to null.”:我们将当前本地上下文对事件侦听器对象的引用设置为 null,这应该允许垃圾回收侦听器。
  • Starting GC.”:垃圾回收开始。
  • GC finished.”:垃圾回收结束,但我们的事件侦听器对象没有被垃圾收集器回收,这可以通过其终结器未被调用的事实来证明。
  • EventListener received event.”:第二次调用“source.Raise()”证实了这一点,侦听器仍然存在!
  • Setting source to null.”:我们将对事件源对象的引用设置为 null。
  • Starting GC.”:第二次垃圾回收开始。
  • NaiveEventListener finalized.”:这次,我们的天真侦听器被回收了,亡羊补牢总比没有好。
  • GC finished.”:第二次垃圾回收结束。

结论:事实上,存在一个隐藏的强引用指向侦听器,这阻止了事件侦听器被回收,直到事件源被回收!

希望有一个标准的解决方案来解决这个问题:事件源可以通过弱引用引用侦听器,即使源仍然存在,也不会阻止收集侦听器。

并且 .NET Framework 中有一个标准的模式及其实现:弱事件模式

弱事件模式

那么,让我们看看如何在 .NET Framework 中解决这个问题。

通常有不止一种方法可以做到这一点,但在这种情况下,决策过程非常直接。

  • 如果您正在使用 .NET 4.5,则可以使用一个简单的实现。
  • 否则,您将不得不依赖一种稍微更棘手的方法。

传统方式

在 .NET 4.5 之前,.NET Framework 提供了一个类和一个接口,允许实现弱事件模式。

(两者都位于 `WindowsBase` 程序集中,如果您不开发已正确引用它的 WPF 项目,则需要自己引用它。)

所以这是一个两步过程。

首先,您需要实现一个自定义事件管理器,通过继承 `WeakEventManager` 类。

  • 您需要重写 StartListeningStopListening 方法,它们分别用于注册新处理程序和注销现有处理程序;`WeakEventManager` 基类本身将使用它们。
  • 您需要提供两种方法来访问侦听器列表,通常命名为“AddListener”和“RemoveListener”,供您的自定义事件管理器用户使用。
  • 您需要提供一种方法来获取当前线程的事件管理器,通常是通过在自定义事件管理器类中公开一个 `static` 属性。

然后,您需要使您的侦听器类实现 `IWeakEventListener` 接口。

  • 您需要实现 ReceiveWeakEvent 方法。
  • 您需要尝试处理事件。
  • 如果您已成功处理了事件,则返回 `true`。

这说了不少话,但它转化为相对较少的代码。

首先,是自定义弱事件管理器。

public class EventManager : WeakEventManager
{
    private static EventManager CurrentManager
    {
        get
        {
            EventManager manager = (EventManager)GetCurrentManager(typeof(EventManager));

            if (manager == null)
            {
                manager = new EventManager();
                SetCurrentManager(typeof(EventManager), manager);
            }

            return manager;
        }
    }

    public static void AddListener(EventSource source, IWeakEventListener listener)
    {
        CurrentManager.ProtectedAddListener(source, listener);
    }

    public static void RemoveListener(EventSource source, IWeakEventListener listener)
    {
        CurrentManager.ProtectedRemoveListener(source, listener);
    }

    protected override void StartListening(object source)
    {
        ((EventSource)source).Event += DeliverEvent;
    }

    protected override void StopListening(object source)
    {
        ((EventSource)source).Event -= DeliverEvent;
    }
}

然后是我们的事件侦听器。

public class LegacyWeakEventListener : IWeakEventListener
{
    private void OnEvent(object source, EventArgs args)
    {
        Console.WriteLine("LegacyWeakEventListener received event.");
    }

    public LegacyWeakEventListener(EventSource source)
    {
        EventManager.AddListener(source, this);
    }

    public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
        OnEvent(sender, e);

        return true;
    }

    ~LegacyWeakEventListener()
    {
        Console.WriteLine("LegacyWeakEventListener finalized.");
    }
}

我们来检查一下。

Console.WriteLine("=== Legacy weak listener (better) ===");

EventSource source = new EventSource();

LegacyWeakEventListener listener = new LegacyWeakEventListener(source);

source.Raise();

Console.WriteLine("Setting listener to null.");
listener = null;

TriggerGC();

source.Raise();

Console.WriteLine("Setting source to null.");
source = null;

TriggerGC();

结果

LegacyWeakEventListener received event.
Setting listener to null.
Starting GC.
LegacyWeakEventListener finalized.
GC finished.
Setting source to null.
Starting GC.
GC finished.

很好,它奏效了,我们的事件侦听器对象现在在第一次 GC 时被正确终结,即使事件源对象仍然存在,不再有泄漏。

但对于一个简单的侦听器来说,写这么多代码相当麻烦,想象一下您有几十个这样的侦听器,您必须为每种类型编写一个新的弱事件管理器!

如果您熟悉代码重构和泛型,您可能会找到一种聪明的方法来重构所有这些公共代码。

在 **.NET 4.5** 之前,您必须自己实现这个聪明的弱事件管理器,但现在 .NET 为这个问题提供了一个标准解决方案,我们将立即审查它!

.NET 4.5 的方式

.NET 4.5 引入了传统 `WeakEventManager` 的新泛型版本:WeakEventManager<TEventSource, TEventArgs>

(此类也位于 `WindowsBase` 程序集中。)

通过良好地利用 .NET 泛型,`WeakEventManager<TEventSource, TEventArgs>` 能够自行处理泛型,而无需我们为每个事件源重新实现新管理器。

因此,生成的代码要轻量且可读得多。

public class WeakEventListener
{
    private void OnEvent(object source, EventArgs args)
    {
        Console.WriteLine("WeakEventListener received event.");
    }

    public WeakEventListener(EventSource source)
    {
        WeakEventManager<EventSource, EventArgs>.AddHandler(source, "Event", OnEvent);
    }

    ~WeakEventListener()
    {
        Console.WriteLine("WeakEventListener finalized.");
    }
}

只需写一行代码,非常干净。

使用方法与其他实现类似,因为所有样板代码都已封装在事件侦听器类中。

Console.WriteLine("=== .Net 4.5 weak listener (best) ===");

EventSource source = new EventSource();

WeakEventListener listener = new WeakEventListener(source);

source.Raise();

Console.WriteLine("Setting listener to null.");
listener = null;

TriggerGC();

source.Raise();

Console.WriteLine("Setting source to null.");
source = null;

TriggerGC();

为了确保它按预期工作,这里是输出。

WeakEventListener received event.
Setting listener to null.
Starting GC.
WeakEventListener finalized.
GC finished.
Setting source to null.
Starting GC.
GC finished.

正如预期的那样,行为与旧事件管理器相同,我们还能要求什么?!

结论

正如您所见,在 .NET 中实现弱事件模式非常简单,尤其是在.NET 4.5 中。

如果您不使用 .NET 4.5,由于实现需要一些样板代码,您可能会倾向于不使用此模式,而是直接使用 C# 语言的设施(+= 和 -=),看看是否会出现任何内存问题,只有在发现一些泄漏后,才付出必要的努力来实现它。

但使用 .NET 4.5,由于几乎没有成本,样板代码由框架管理,您可以真正地首先使用它,尽管它比 C# 的“+=”和“-=”语法有点不那么酷,但语义同样清晰,这才是最重要的。

我已尽我最大的努力在技术上准确并避免任何拼写错误,但如果您发现任何错字或错误,对代码有疑问或有其他问题,请随时发表评论。

历史

  • 2014 年 3 月 3 日:初始版本
© . All rights reserved.