详解C#编程中.NET的弱事件模式

引言

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

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

  • “传统”方法 (嗯,在 .Net 4.5 前,所以也没那么老),它实现起来比较繁琐
  • .Net 4.5 框架提供的新方法,它则是尽其可能的简单

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

从常见事物开始

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

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

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() 确保新生成的对象也被清理了

引入问题

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

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

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

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

事件处理问题

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

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

例子

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

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

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.“:第二次垃圾回收完成.

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

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

这里有一个标准的模式及其在.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 ,那么你将从简单的实现受益
  • 另外,你必须依靠一点人为的技巧手段

传统方式

  • WeakEventManager 是所有模式管道的封装
  • IWeakEventListener 是管道,它允许一个组件连接到WeakEventManager管件

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

因此这有两步处理.

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

  • 重写 StartListening 和 StopListening 方法,分别注册一个新的handler和注销一个已存在的; 它们将被WeakEventManager基类使用。
  • 提供两个方法来访问listener列表, 命名为 “AddListener” 和 “RemoveListener “,给自定义事件管理器的使用者使用。
  • 通过在自定义事件管理器上暴露一个静态属性,提供一个方式去获得当前线程的事件管理器。
  • 之后使listenr实现IWeakEventListenr接口:
  • 实现 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;
  }
}

之后是事件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提供一个标准的解决方案来解决这个问题了,现在就来回顾下吧!

 .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.AddHandler(source, "Event", OnEvent);
  }

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

简单的一行代码,真简洁.

其他实现的使用也是相似的, 就是装入所有东西到事件listener类里:

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# (+= and -=), 看看是否有内存问题,如果注意到泄露,还需要花必要的时间去实现一个。

但是用 .Net 4.5, 它是自由和简洁,而且由框架管理, 你可以毫无顾虑的选择它, 尽管没有 C# 语法 “+=” 和 “-=” 的酷, 但是语义是清晰的,这才是最重要的.

(0)

相关推荐

  • C#获取USB事件API实例分析

    本文实例讲述了C#获取USB事件API.分享给大家供大家参考.具体如下: const int WM_DEVICECHANGE = 0x2190; const int DBT_DEVICEARRIVAL = 0x8000; const int DBT_DEVICEREMOVECOMPLETE = 0x8004; protected override void WndProc(ref Message m) { try { //if (m.Msg == WM_DEVICECHANGE) //{ swi

  • C#事件用法实例浅析

    本文实例讲述了C#事件用法.分享给大家供大家参考.具体分析如下: EventHandler<TEventArgs>的定义如下 public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e) where TEventArgs:EventArgs 第一个参数必须是object类型(是一个对象,包含事件的发送者) 第二个参数是T类型(即泛型),定义了一个T的约束,它必须派生自基类EventArgs Car

  • C#自定义事件及用法实例

    本文实例讲述了C#自定义事件及用法.分享给大家供大家参考.具体分析如下: 事件是C#中一个重要的内容,MSDN上有一个自定义事件的演示示例.我看了半天有点晕,所以新建了一个winform工程添加了一个按钮,然后找出调用的程序,一对比做了一个类似的示例,就明白了.看代码有时候比看文档来得更快. 所以还是一贯的原则,来干的,不来稀的. using System; namespace TestEventArgs { /// <summary> /// 这个类对应于EventArgs,做对比学习. /

  • 理解C#中的事件

    前面文章中介绍了委托相关的概念,委托实例保存这一个或一组操作,程序中将在某个特定的时刻通过委托实例使用这些操作. 如果做过GUI程序开发,可能对上面的描述会比较熟悉.在GUI程序中,单击一个button会触发一个click事件,然后会执行一系列的操作,这一系列的操作就被存放在一个委托实例中. 接下来我们就看看事件. 使用委托中的问题 回到前面文章中苹果和富士康的例子,苹果将iphone的组装.包装和运输的工作全部委托给了富士康. 根据上面的描述,我们修改了一下代码,在Apple这个类中加入一个订

  • C#移除所有事件绑定的方法

    本文实例讲述了C#移除所有事件绑定的方法.分享给大家供大家参考.具体分析如下: private delegate int DEL_TEST_EventHandler(int m, int n); private event DEL_TEST_EventHandler DelTestEventHandler; /// <summary> /// 移除所有的事件绑定 /// </summary> /// <param name="clearEvent">

  • C#自定义事件监听实现方法

    本文实例讲述了C#自定义事件监听实现方法.分享给大家供大家参考.具体实现方法如下: using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApp { /// <summary> /// 定义事件 /// </summary> class CustomEvent { /// <summary> /// 定义委托 /// &

  • C#动态生成按钮及定义按钮事件的方法

    本文实例讲述了C#动态生成按钮及定义按钮事件的方法.分享给大家供大家参考.具体实现方法如下: 1.后台生成input的button按钮 复制代码 代码如下: HtmlGenericControl control = new HtmlGenericControl("input"); control.Attributes.Add("type", "button"); control.Attributes.Add("onclick"

  • 在C#使用字典存储事件示例及实现自定义事件访问器

    使用字典存储事件实例 accessor-declarations 的一种用法是公开很多事件但不为每个事件分配字段,而是使用字典来存储这些事件实例.这只在具有很多事件但您预计大多数事件都不会实现时才有用. public delegate void EventHandler1(int i); public delegate void EventHandler2(string s); public class PropertyEventsSample { private System.Collecti

  • C#委托与事件初探

    委托给了C#操作函数的灵活性,我们可使用委托像操作变量一样来操作函数,其实这个功能并不是C#的首创,早在C++时代就有函数指针这一说法,而在我看来委托就是C#的函数指针,首先先简要的介绍一下委托的基本知识: 委托的定义 委托的声明原型是 delegate <函数返回类型> <委托名> (<函数参数>) 例子:public delegate void CheckDelegate(int number);//定义了一个委托CheckDelegate,它可以注册返回void类

  • 结合Visual C#开发环境讲解C#中事件的订阅和取消订阅

    类或对象可以通过事件向其他类或对象通知发生的相关事情.发送(或引发)事件的类称为"发行者",接收(或处理)事件的类称为"订户". 在典型的 C# Windows 窗体或 Web 应用程序中,可订阅由控件(如按钮和列表框)引发的事件.可使用 Visual C# 集成开发环境 (IDE) 来浏览控件发布的事件,选择要处理的事件.IDE 会自动添加空事件处理程序方法和订阅事件的代码. 事件概述 事件具有以下特点: 发行者确定何时引发事件,订户确定执行何种操作来响应该事件.

  • C#中事件的定义和使用

    事件的声明和使用与代理有很密切的关系,事件其实是一个或多个方法的代理,当对象的某个状态发生了变化,代理会被自动调用,从而代理的方法就被自动执行. 声明和使用一个事件需要如下步骤: 1.创建一个代理. 2.在类的内部利用event关键字声明事件,并且在类中定义调用事件的方法,也可以定义一个处理事件消息的方法. 声明一个事件的基本形式有两种: 修饰符  event   类型   标识符 修饰符  event   类型   标识符{get{};set{};} 其中: 修饰符是指C#语言的访问修饰符:类

  • C#通过委托调用Button单击事件的方法

    这里介绍通过委托取消Button事件switch-case的方法.需要注意的是,事先要按顺序在各个Button的Tag属性中设置0.1.2.3--等序号,其作用请详看代码. /*定义委托*/ public delegate 类型或viod MethodDelegate(参数1, 参数2); private void buttonC_Click(object sender, EventArgs e) { Button button = (Button)sender; /*向委托添加方法*/ Met

  • 结合.net框架在C#派生类中触发基类事件及实现接口事件

    在派生类中引发基类事件 以下简单示例演示了在基类中声明可从派生类引发的事件的标准方法.此模式广泛应用于 .NET Framework 类库中的 Windows 窗体类. 在创建可用作其他类的基类的类时,应考虑如下事实:事件是特殊类型的委托,只可以从声明它们的类中调用.派生类无法直接调用基类中声明的事件.尽管有时需要事件仅由基类引发,但在大多数情形下,应该允许派生类调用基类事件.为此,您可以在包含该事件的基类中创建一个受保护的调用方法.通过调用或重写此调用方法,派生类便可以间接调用该事件. 注意:

随机推荐