C# 清理事件处理程序引用的最佳实践是什么?

C# 清理事件处理程序引用的最佳实践是什么?,c#,events,garbage-collection,dispose,C#,Events,Garbage Collection,Dispose,我经常发现自己在写这样的代码: if (Session != null) { Session.KillAllProcesses(); Session.AllUnitsReady -= Session_AllUnitsReady; Session.AllUnitsResultsPublished -= Session_AllUnitsResultsPublished; S

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

        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();
        }
    /// <summary>
    /// Disposes the object
    /// </summary>
    public void Dispose()
    {
        SubmitRequested = null; //frees all references to the SubmitRequested Event
    }
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 其目的是清理我们在目标(会话)上注册的所有事件订阅,以便会话可以由GC自由处置。我与一位同事讨论了实现IDisposable的类,但他认为这些类应该执行如下清理:

        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();
        }
    /// <summary>
    /// Disposes the object
    /// </summary>
    public void Dispose()
    {
        SubmitRequested = null; //frees all references to the SubmitRequested Event
    }
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
//
///处理对象
/// 
公共空间处置()
{
SubmitRequested=null;//释放对SubmitRequested事件的所有引用
}
是否有理由选择其中一个而不是另一个?有没有更好的办法来解决这个问题?(除了各地的弱参考事件)


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

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

  • 它是标准的,编译器对它进行特殊处理。这意味着阅读您的代码的每个人在看到IDisposable被实现的那一刻都了解代码的内容
  • .NET C#和VB通过
    using
    语句提供了使用IDisposable的特殊构造 不过,我怀疑这在您的场景中是否有用。要安全地处置对象,需要在try/catch中的finally块中处置该对象。在您似乎描述的情况下,可能需要会话或调用会话的代码在删除对象时(即,在其作用域的末尾:在finally块中)处理此问题。如果是这样的话,会话也必须实现IDisposable,这遵循了通用的概念。在IDisposable.Dispose方法中,它循环遍历其所有可丢弃的成员并对其进行处置

    编辑 你最近的评论让我重新思考我的答案,试着把几个点连起来。您希望确保会话可由GC使用。如果对委托的引用来自同一个类,则根本不需要取消订阅它们。如果他们来自另一个班级,你需要取消订阅。查看上面的代码,您似乎在使用会话的任何类中编写了该代码块,并在过程中的某个时刻将其清理干净

    如果会话需要释放,有一种更直接的方法,调用类不需要负责正确处理取消订阅过程。简单地使用琐碎的反射来循环所有事件,并将所有设置为空(可以考虑其他方法来达到相同的效果)。
    因为您需要“最佳实践”,所以应该将此方法与
    IDisposable
    结合起来,并在
    IDisposable.Dispose()中实现循环。在进入此循环之前,调用另一个事件:
    Disposing
    ,如果侦听器需要自己清理任何内容,可以使用该事件。使用IDisposable时,请注意它的警告,这是一个常见的解决方案。

    如果说从
    会话
    事件中注销处理程序将以某种方式允许GC收集
    会话
    对象,这是不正确的。下面是一个图表,说明了事件的参考链

    --------------      ------------      ----------------
    |            |      |          |      |              |
    |Event Source|  ==> | Delegate |  ==> | Event Target |
    |            |      |          |      |              |
    --------------      ------------      ----------------
    
    因此,在您的例子中,事件源是一个
    会话
    对象。但是我没有看到您提到哪个类声明了处理程序,所以我们还不知道事件目标是谁。让我们考虑两种可能性。事件目标可以是表示源的同一个
    会话
    对象,也可以是一个完全独立的类。在这两种情况下,在正常情况下,只要没有对的另一个引用,即使对其事件的处理程序保持注册,也将收集会话。这是因为委托不包含对事件源的引用。它只包含对事件目标的引用

    考虑以下代码

    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) { }
    }
    
    您将看到“disposed”被打印两次到控制台,以验证两个实例都是在未注销事件的情况下收集的。被
    test2
    引用的对象之所以被收集,是因为它在引用图中仍然是一个孤立的实体(一旦
    test2
    设置为空,即为空),即使它通过事件返回了对自身的引用

    现在,当您希望事件目标的生命周期比事件源的生命周期短时,事情就变得棘手了。在这种情况下,您必须注销事件。考虑下面的代码来演示这一点。

    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;
      }
    }
    
    您将看到“disposed”从未打印到控制台,这表明可能存在内存泄漏。这是一个特别难处理的问题。在
    Child
    中实现
    IDisposable
    并不能解决问题,因为无法保证调用者会玩得很好并实际调用
    Dispose

    答案

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

    如果您的事件目标实现了
    IDisposable
    ,则它可以从事件源中清除自身,但无法保证
    Dispose
    将被调用


    我并不是说从
    Dispose
    注销事件是错误的。我的观点是,你真的需要检查你的类层次结构是如何定义的,并且考虑到如果存在的话,你如何最好地避免内存泄漏问题。

    < p>我发现执行简单的任务,比如将最常用的事件归类到它们自己的类中并继承它们的接口有助于为开发者提供一个使用的机会。用于添加和删除事件的方法,如事件属性。在内部
    Disposable.Create(() => {
                    this.ViewModel.Selection.CollectionChanged -= SelectionChanged;
                })
    
    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());
    
    eventsToDispose.Dispose();