开源中国

我们不支持 IE 10 及以下版本浏览器

It appears you’re using an unsupported browser

为了获得更好的浏览体验,我们强烈建议您使用较新版本的 Chrome、 Firefox、 Safari 等,或者升级到最新版本的IE浏览器。 如果您使用的是 IE 11 或以上版本,请关闭“兼容性视图”。
C#中的 .NET 弱事件模式 - 技术翻译 - 开源中国社区

C#中的 .NET 弱事件模式 【已翻译100%】

标签: C#
oschina 推荐于 4年前 (共 12 段, 翻译完成于 03-08) 评论 11
收藏  
94
推荐标签: C# 待读

引言

你可能知道,事件处理内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由。

在这个短文中(期望如此),我会在 .Net 框架的上下文事件处理中展示这个问题,之后我会教你这个问题的标准解决方案,弱事件模式。有两种方法,即:

  • “传统”方法 (嗯,在 .Net 4.5 前,所以也没那么老),它实现起来比较繁琐

  • .Net 4.5 框架提供的新方法,它则是尽其可能的简单

(源代码在 这里 可供使用。)

yxrykds
 翻译得不错哦!

从常见事物开始

在一头扎进本文核心内容前,让我们回顾一下在代码中最常使用的两个事物:类和方法。

事件源

让我为您介绍一个基本但很有用的事件源类,它最低限度地揭示了足够的复杂性来说明这一点:

public class EventSource
{
    public event EventHandlerEvent = delegate { };

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

对好奇那个奇怪的空委托初始化方法(delegate { })的人来说,这是一个用来确保事件总被初始化的技巧,这样就可以不必每次在使用它之前都要检查它是否不为NULL。

赵亮-碧海情天
 翻译得不错哦!

触发垃圾收集的实用方法

在.net中,垃圾收集以一种不确定的方式触发。这对我们的实验很不利,我们的实验需要以一种确定的方式跟踪对象的状态。

所以,我们必须定期触发自己的垃圾收集操作,同时避免复制管道代码,管道代码已经在在一个特定的方法中释放:

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

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

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

虽然不是很复杂,但是如果你不是很熟悉这种模式,还是有必要小小解释一下:

  • 第一个 GC.Collect() 触发.net的CLR垃圾收集器,对于负责清理不再使用的对象,和那些类中没有终结器(即c#中的析构函数)的对象,CLR垃圾收集器足够胜任

  • GC.WaitForPendingFinalizers() 等待其他对象的终结器执行;我们需要这样做,因为,你将看到我们使用终结器方法去追踪我们的对象在什么时候被收集的

  • 第二个GC.Collect() 确保新生成的对象也被清理了

coinci
 翻译得不错哦!

引入问题

首先让我们试着通过一些理论,最重要的是还有一个演示的帮助,去了解事件监听器有哪些问题。

背景

一个对象要想被作为事件侦听器,需要将其实例方法之一登记为另一个能够产生事件的对象(即事件源)的事件处理程序,事件源必须保持一个到事件侦听器对象的引用,以便在事件发生时调用此侦听器的处理方法。

这很合理,但如果这个引用是一个 强引用,则侦听器会作为事件源的一个依赖 从而不能作为垃圾回收,即使引用它的最后一个对象是事件源。

赵亮-碧海情天
 翻译得不错哦!

下面详细图解在这下面发生了什么:

事件处理问题

这将不是一个问题,如果你可以控制listener object的生命周期,你可以取消对事件源的订阅当当你不再需要listener,常常可以使用disposable pattern(用后就扔的模式)。

但是如果你不能在listener生命周期内验证单点响应,在确定性的方式中你不能把它处理掉,你必须依赖GC处理...这将从不会考虑你所准备的对象,只要事件源还存在着!

MeiKai
 翻译得不错哦!

例子

理论都是好的,但还是让我们看看问题和真正的代码。

这是我们勇敢的时间监听器,还有点幼稚,我们很快知道为什么:

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()”的结果; perfect, seems like we’re listening.

  • Setting listener to null.“: 我们把本地事件监听器对象引用赋空值,这样应该可以让垃圾回收器回收了.

  • Starting GC.“: 垃圾回收开始.

  • GC finished.“: 垃圾回收开始, 但是 但是我们的事件监听器没有被回收器回收, 这样就证明了事件监听器的析构函数没有被调用。

  • EventListener received event.“: 第二次调用 “source.Raise()”来确认,发现这监听器还活着。

  • Setting source to null.“: 我们在赋空值给事件的原对象.

  • Starting GC.“: 第二次垃圾回收.

  • NaiveEventListener finalized.“: 这一次幼稚的事件监听终于被回收了,迟到总好过没有.

  • GC finished.“:第二次垃圾回收完成.

Idiot_s_Sky
 翻译得不错哦!

结论:确实有一个隐藏的对事件监听器的强引用,目的是防止它在事件源被回收之前被回收!

希望有针对此问题的标准解决方案:让事件源可以通过弱引用来引用侦听器,在事件源存在时也可以回收侦听器对象。

这里有一个标准的模式及其在.NET框架上的实现:弱事件模式(http://msdn.microsoft.com/en-us/library/aa970850.aspx)。 And there is a standard pattern and its implementation in the .Net framework: the weak event pattern.

赵亮-碧海情天
 翻译得不错哦!

弱事件模式

让我们看看在.NET中如何应付这个问题,

通常有超过一种方法去做,但是在这种情况下可以直接决定:

  • 如果你正在使用 .Net 4.5 ,那么你将从简单的实现受益

  • 另外,你必须依靠一点人为的技巧手段

传统方式

(这两个位于WindowBase程序集,你将需要参考你自己的如果你不在开发WPF项目,你应该准确的参考WindowBase)

MeiKai
 翻译得不错哦!

因此这有两步处理.

首先通过继承WeakEventManager来实现一个自定义事件管理器:

  • 重写 StartListening 和 StopListening 方法,分别注册一个新的handler和注销一个已存在的; 它们将被WeakEventManager基类使用。

  • 提供两个方法来访问listener列表, 命名为 “AddListener” 和 “RemoveListener “,给自定义事件管理器的使用者使用。

  • 通过在自定义事件管理器上暴露一个静态属性提供一个方式去获得当前线程的事件管理器

之后使listenr实现IWeakEventListenr接口:

  • 实现 ReceiveWeakEvent 方法

  • 尝试去处理这个事件

  • 如果无误的处理好事件,将返回true

Idiot_s_Sky
 翻译得不错哦!

有很多要说的,但是可以相对地转换成一些代码:

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

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;
    }
}

之后是事件listener:

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.

非常好,它起作用了,我们的事件listener对象现在可以在第一次GC里正确的析构,即使事件源对象还存活,不再泄露内存了.

但是要写一堆代码就为了一个简单的listener,想象一下你有一堆这样的listener,你必须要为每个类型的写一个弱事件管理器!

如果你很擅长代码重构,你可以发现一个聪明的方式去重构所有通用的代码.

.Net 4.5 出现之前,你必须自己实现弱事件管理器,但是现在,.Net提供一个标准的解决方案来解决这个问题了,现在就来回顾下吧!

Idiot_s_Sky
 翻译得不错哦!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
评论(11)
Ctrl/CMD+Enter

不错,大手啊,俺还没研究到这个地步啊
算是比较有深度的文章了。可惜翻译的不太流畅。
希望OSC以后多一点.NET的文章啊

引用来自“TerryMa”的评论

希望OSC以后多一点.NET的文章啊

确实挺少的
+=很酷啊,我就很讨厌vb的那种addHandle写法,太二了。

引用来自“任意球”的评论

引用来自“TerryMa”的评论

希望OSC以后多一点.NET的文章啊

确实挺少的

哪怕是多点.NET MVC的文章也行,毕竟.NET MVC也是开源的。
结论记住WeakEventManager这个类
有味道,有深度,赞!
新学到了一个概念, 弱事件
不知道java有没有大量内存泄漏的问题
是不是我有一个事件发布者,就要对应一个event manager, 有没有全局管理的方案?
顶部