清理事件处理程序引用的最佳做法是什么?

时间:2010-07-15 17:06:46

标签: c# events garbage-collection dispose

我经常发现自己编写这样的代码:

        if (Session != null)
        {
            Session.KillAllProcesses();
            Session.AllUnitsReady -= Session_AllUnitsReady;
            Session.AllUnitsResultsPublished -= Session_AllUnitsResultsPublished;
            Session.UnitFailed -= Session_UnitFailed;
            Session.SomeUnitsFailed -= Session_SomeUnitsFailed;
            Session.UnitCheckedIn -= Session_UnitCheckedIn;
            UnattachListeners();
        }

目的是清理我们在目标(会话)上注册的所有事件订阅,以便GC可以自由处理会话。我和一位同事讨论了实现IDisposable的类,但他相信这些类应该像这样进行清理:

    /// <summary>
    /// Disposes the object
    /// </summary>
    public void Dispose()
    {
        SubmitRequested = null; //frees all references to the SubmitRequested Event
    }

是否有理由选择其中一个?有没有更好的方法来解决这个问题? (除了各处的弱参考事件)

我真正希望看到的是一些类似于引发事件的安全调用模式:即安全和可重复。我每次附加到活动时都记得做的事情,这样我就能确保清理起来很容易。

6 个答案:

答案 0 :(得分:41)

说从Session事件取消注册处理程序将以某种方式允许GC收集Session对象是不正确的。这是一个说明参考事件链的图表。

--------------      ------------      ----------------
|            |      |          |      |              |
|Event Source|  ==> | Delegate |  ==> | Event Target |
|            |      |          |      |              |
--------------      ------------      ----------------

因此,在您的情况下,事件源是Session对象。但我没有看到你提到哪个类声明了处理程序,所以我们还不知道事件目标是谁。让我们考虑两种可能性。事件目标可以是表示源的相同Session对象,也可以是完全独立的类。在任何一种情况下,在正常情况下,只要没有其他引用,即使其事件的处理程序仍然注册,也将收集Session。这是因为委托不包含对事件源的引用。它只包含对事件目标的引用。

请考虑以下代码。

public static void Main()
{
  var test1 = new Source();
  test1.Event += (sender, args) => { Console.WriteLine("Hello World"); };
  test1 = null;
  GC.Collect();
  GC.WaitForPendingFinalizers();

  var test2 = new Source();
  test2.Event += test2.Handler;
  test2 = null;
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

public class Source()
{
  public event EventHandler Event;

  ~Source() { Console.WriteLine("disposed"); }

  public void Handler(object sender, EventArgs args) { }
}

您将看到“已处置”两次打印到控制台,验证是否已收集这两个实例而未取消注册该事件。收集test2引用的对象的原因是因为它在参考图中仍然是一个孤立的实体(一旦将test2设置为null),即使它通过事件返回自身也是如此

现在,事情变得棘手的是当你想让事件目标的生命周期短于事件源时。在这种情况下,您取消注册事件。请考虑以下代码来演示这一点。

public static void Main()
{
  var parent = new Parent();
  parent.CreateChild();
  parent.DestroyChild();
  GC.Collect();
  GC.WaitForPendingFinalizers();
}

public class Child
{
  public Child(Parent parent)
  {
    parent.Event += this.Handler;
  }

  private void Handler(object sender, EventArgs args) { }

  ~Child() { Console.WriteLine("disposed"); }
}

public class Parent
{
  public event EventHandler Event;

  private Child m_Child;

  public void CreateChild()
  {
    m_Child = new Child(this);
  }

  public void DestroyChild()
  {
    m_Child = null;
  }
}

您将看到“处理”从未打印到控制台,以显示可能的内存泄漏。这是一个特别难以处理的问题。在IDisposable中实施Child无法解决问题,因为没有保证呼叫者能够很好地播放并实际呼叫Dispose

答案

如果你的事件源实现了IDisposable,那么你还没有真正给自己买任何新东西。这是因为如果事件源不再是根目录,那么事件目标将不再具有root权限。

如果您的事件目标实现IDisposable,那么它可以从事件源中清除自己,但是没有保证Dispose将被调用。

我不是说来自Dispose的未注册事件是错误的。我的观点是,您确实需要检查如何定义类层次结构,并考虑如果存在内存泄漏问题,最好如何避免内存泄漏问题。

答案 1 :(得分:4)

实施IDisposable比手动方法有两个优点:

  1. 这是标准配置,编译器会特别对待它。这意味着每个阅读代码的人都能理解他们看到IDisposable正在实施的那一刻。
  2. .NET C#和VB提供了通过using语句处理IDisposable的特殊构造。
  3. 尽管如此,我怀疑这在你的场景中是否有用。为了安全地处理对象,需要在try / catch中的finally块中处理它。在您似乎描述的情况下,在删除对象时(即,在其范围的末尾:在finally块中),可能要求Session处理此事件或调用Session的代码。如果是这样,Session也必须实现IDisposable,这遵循共同的概念。在IDisposable.Dispose方法中,它遍历所有可丢弃的成员并处理它们。

    修改

    您的最新评论让我重新思考我的答案并尝试连接几个点。您希望GC确保Session是一次性的。如果对代理的引用来自同一个类,则根本不需要取消订阅它们。如果他们来自其他班级,您需要取消订阅。查看上面的代码,您似乎在任何使用Session的类中编写该代码块,并在此过程中的某个时刻对其进行清理。

    如果需要释放Session,则有一种更直接的方式是调用类需要不负责正确处理取消订阅过程。简单地使用平凡反射循环所有事件并将all设置为null(您可以考虑使用其他方法来达到相同的效果)。

    因为您要求“最佳做法”,所以您应该将此方法与IDisposable结合使用,并在IDisposable.Dispose()内实施循环。在进入此循环之前,再调用一个事件:Disposing,如果需要自己清理任何内容,监听器可以使用这些事件。使用IDisposable时,请注意其警告,this briefly described pattern是一种常见的解决方案。

答案 2 :(得分:3)

使用vb.net WithEvents关键字自动生成的事件处理模式非常不错。 VB代码(大致):

WithEvents myPort As SerialPort

Sub GotData(Sender As Object, e as DataReceivedEventArgs) Handles myPort.DataReceived
Sub SawPinChange(Sender As Object, e as PinChangedEventArgs) Handles myPort.PinChanged

将被翻译成相当于:

SerialPort _myPort;
SerialPort myPort
{  get { return _myPort; }
   set {
      if (_myPort != null)
      {
        _myPort.DataReceived -= GotData;
        _myPort.PinChanged -= SawPinChange;
      }
      _myPort = value;
      if (_myPort != null)
      {
        _myPort.DataReceived += GotData;
        _myPort.PinChanged += SawPinChange;
      }
   }
}

这是一个合理的模式;如果您使用此模式,那么在Dispose中,您将设置null所有具有相关事件的属性,这些属性将依次取消订阅它们。

如果想要稍微自动化处理,以确保处理完毕,可以将属性更改为:

Action<myType> myCleanups; // Just once for the whole class
SerialPort _myPort;
static void cancel_myPort(myType x) {x.myPort = null;}
SerialPort myPort
{  get { return _myPort; }
   set {
      if (_myPort != null)
      {
        _myPort.DataReceived -= GotData;
        _myPort.PinChanged -= SawPinChange;
        myCleanups -= cancel_myPort;
      }
      _myPort = value;
      if (_myPort != null)
      {
        myCleanups += cancel_myPort;
        _myPort.DataReceived += GotData;
        _myPort.PinChanged += SawPinChange;
      }
   }
}
// Later on, in Dispose...
  myCleanups(this);  // Perform enqueued cleanups

请注意,将静态委托挂钩到myCleanups意味着即使有myClass的许多实例,也只需要系统范围内每个委托的一个副本。对于具有少量实例的类而言,这可能不是什么大问题,但如果一个类将被实例化数千次,则可能很重要。

答案 3 :(得分:2)

我的偏好是使用一次性管理生命。 Rx包含一些一次性扩展,可以让您执行以下操作:

Disposable.Create(() => {
                this.ViewModel.Selection.CollectionChanged -= SelectionChanged;
            })

如果您将其存储在某种类型的GroupDisposable中,这些GroupDisposable的生命周期正确,那么您就已经完成了设置。

如果您没有使用一次性用品和示波器管理生命,那么绝对值得研究,因为它在.net中成为一种非常普遍的模式。

答案 4 :(得分:1)

我发现执行简单的任务,如全局化最常用于其自己的类的事件,并继承其接口有助于为开发人员提供使用事件属性等方法添加和删除事件的机会。在您的类中,无论是否进行封装,都可以使用类似于以下示例的内容开始清理。

E.g。

#region Control Event Clean up
private event NotifyCollectionChangedEventHandler CollectionChangedFiles
{
    add { FC.CollectionChanged += value; }
    remove { FC.CollectionChanged -= value; }
}
#endregion Control Event Clean up

这篇文章为Property ADD REMOVE的其他用途提供了额外的反馈: http://msdn.microsoft.com/en-us/library/8843a9ch.aspx

答案 5 :(得分:0)

DanH的答案几乎存在,但缺少一个关键要素。

要使其始终正常运行,必须先获取变量的本地副本,以防更改。本质上,我们必须强制执行一个隐式捕获的闭包。

List<IDisposable> eventsToDispose = new List<IDisposable>();

var handlerCopy = this.ViewModel.Selection;
eventsToDispose.Add(Disposable.Create(() => 
{
    handlerCopy.CollectionChanged -= SelectionChanged;
}));

然后,我们可以使用以下方法处理所有事件:

foreach(var d in eventsToDispose)
{ 
    d.Dispose();
}

如果我们想使其更短:

eventsToDispose.ForEach(o => o.Dispose());

如果我们想使其更短,可以用CompositeDisposable替换IList,这在幕后是完全一样的。

然后我们可以使用以下方法处理所有事件:

eventsToDispose.Dispose();
相关问题