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

问题描述 投票:31回答:6

我常常发现自己编写的代码如下:

        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
    }

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

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

c# events garbage-collection dispose
6个回答
42
投票

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

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

答案

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

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

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


5
投票

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

  1. 它是标准的,编译器会特别对待它。 这意味着每个阅读代码的人都会理解他们看到IDisposable被实施的那一刻。
  2. .NET C#和VB提供了通过using语句处理IDisposable的特殊构造。

不过,我怀疑这在你的场景中是否有用。 为了安全地处理对象,需要在try / catch中的finally块中处理它。 在您似乎描述的情况下,在删除对象时(或者在其范围的末尾:在finally块中),可能要求Session处理此事件或调用Session的代码。 如果是这样,Session也必须实现IDisposable,这遵循共同的概念。 在IDisposable.Dispose方法中,它遍历所有可丢弃的成员并处理它们。

编辑

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

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

因为您要求“最佳实践”,所以应该将此方法与IDisposable结合使用,并在IDisposable.Dispose()实现循环。 在进入此循环之前,您再调用一个事件: Disposing ,如果需要自己清理任何内容,哪些侦听器可以使用。 使用IDisposable时,请注意其注意事项,其中简要描述的模式是一种常见的解决方案。


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
投票

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

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

如果你把它存储在某种类型的GroupDisposable中,这些GroupDisposable的生命周期正确,那么你就已经完成了。

如果你不管理一次性用品和范围,那么绝对值得研究,因为它在.net中成为一种非常普遍的模式。


1
投票

我发现执行简单的任务,如全局化最常用于其自己的类的事件,继承它们的接口有助于开发人员有机会使用诸如事件属性之类的方法来添加和删除事件。 在您的类中,是否发生封装可以通过使用类似于以下示例的内容开始清理。

例如

#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


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();
© www.soinside.com 2019 - 2024. All rights reserved.