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

Fir*_*oso 32 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();
        }
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Bri*_*eon 43

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

--------------      ------------      ----------------
|            |      |          |      |              |
|Event Source|  ==> | Delegate |  ==> | Event Target |
|            |      |          |      |              |
--------------      ------------      ----------------
Run Code Online (Sandbox Code Playgroud)

所以在你的情况下,事件源是一个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) { }
}
Run Code Online (Sandbox Code Playgroud)

您将看到"已处置"两次打印到控制台,验证是否收集了两个实例而未取消注册该事件.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;
  }
}
Run Code Online (Sandbox Code Playgroud)

您将看到"处理"从未打印到控制台,表明可能存在内存泄漏.这是一个特别难以处理的问题.实施IDisposableChild,因为没有一种机制保障时,呼叫者会发挥很好,实际上调用不会解决问题Dispose.

答案

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

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

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

  • @KyleBaran:是的,没错.重要的是,没有别的东西可以引用那个块中的任何东西.如果存在对块中某个对象的引用,并且该对象将手指扩展到块中的其他对象,则所有投注都将关闭. (2认同)

Abe*_*bel 5

与手动方法相比,实现IDisposable具有两个优点:

  1. 它是标准的,编译器会对其进行特殊处理。这意味着阅读您的代码的每个人都知道IDisposable正在被实施的那一刻。
  2. .NET C#和VB通过该using语句为使用IDisposable提供了特殊的构造。

不过,我怀疑这在您的情况下是否有用。为了安全地处置对象,需要将其处置在try / catch中的finally块中。在您似乎要描述的情况下,可能需要在删除对象时(即,在其作用域的末尾:finally块中)由Session来处理此问题,或者要求调用Session的代码进行处理。如果是这样,则会话也必须实现IDisposable,它遵循通用概念。在IDisposable.Dispose方法内部,它循环遍历所有可抛弃的成员并将其处置。

编辑

您的最新评论使我重新思考了我的答案,并尝试连接一些点。您想确保Session可被GC使用。如果对委托的引用来自同一类内部,则完全不需要取消订阅。如果它们来自其他班级,则需要取消订阅。查看上面的代码,您似乎可以在使用Session的任何类中编写该代码块,并在过程中的某个时刻对其进行清理。

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

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