【问题标题】:What to do with delegate / event references in a class that implements IDisposable如何处理实现 IDisposable 的类中的委托/事件引用
【发布时间】:2012-09-24 13:58:19
【问题描述】:

我一直在阅读有关内存管理的内容,并且在一个项目中遇到了这样的情况,即这本书和谷歌都没有给出确切的答案。我已经知道委托是管理对象,事件是委托实例。话虽如此,一旦应用程序结束,委托实例将从内存中删除。

我想不通的是如何确保在我的类被释放时(显式地或通过 GC)外部代码已释放所有事件引用。例如,A 类公开一个事件,B 类使用它。 B 类在 A 类上调用 Dispose,而不释放对委托的引用。当然,我们不能从 Dispose 方法本身抛出错误。

下面是一个类,它有一个委托和另一个消费它的类。

public class ClassB
{
    private ClassA A { get; set; }

    public ClassB()
    {
        this.A = new ClassA();
        this.A.OnProcessed += new ClassA.DelegateProcessed(this.ClassA_Processed);
    }

    public void Process()
    {
        this.A.Process();
    }

    public void ClassA_Processed (ClassA sender, EventArgs e)
    {
        // Do something.

        // Code written by another developer does not free up events before calling Dispose.

        this.A.Dispose();
        this.A = null;
    }
}

public class ClassA: IDisposable
{
    public delegate void DelegateProcessed (A sender, EventArgs e);
    public event DelegateProcessed OnProcessed = null;

    ~ClassA() { this.Dispose(false); }

    public void Dispose ()
    {
        this.Dispose(true);
        System.GC.SuppressFinalize(this);
    }

    private void Dispose (bool disposing)
    {
        if (!this.Disposed)
        {
            if (disposing)
            {
                // Dispose managed resources here.
                // Is it possible / advisable to dispose of delegates / events here?
                // Will this adversely affect the consumer class?
                this.OnProcessed -= new ClassA.DelegateProcessed(this.ClassA_Processed);
            }
        }
        this.Disposed = true;
    }

    public void Process () { this.OnProcessed(this, new EventArgs()); }

    public void ClassA_Processed (ClassA sender, EventArgs e) { }
}

关键是要确保无论开发人员对 ClassB 做什么,ClassA 都有资格进行垃圾回收。关键是尽量减少 ClassA 在内存中花费的时间,即使消费者粗心。

更新:从答案中可以清楚地看出,事件不必从 ClassA 中显式删除。至于主要问题,弱引用似乎是下面回答的方法。目标是最小化 ClassA 在内存中的停留时间。如果我忽略了任何事情,请告诉我。

【问题讨论】:

  • 这里的逻辑有些前后矛盾。在 OnProcessed 的调用列表中,A 类将持有 B 类的代表,反之亦然。
  • @spender:我在 SO 编辑器中编写了代码,因此可能存在错误,但我不理解您的观点。 ClassA 是具有委托的,而 ClassB 持有引用。我试图从 ClassA 中删除引用,这样即使 ClassB 忘记了,ClassA 也有资格进行垃圾收集。如果您发现错误,请告诉我,我会做必要的。
  • 您可以安全地让 A 类的实例超出范围,而无需我们在您的代码中看到的任何东西通过委托实例保留它。但是,在您的代码中,如果您让 B 的实例超出范围,它将不会被收集,因为在您的实例中,ClassA_Processed 的调用列表中保留了一个指向方法 ClassA_Processed 的委托A类
  • 客户端程序员会期望在他处理您的对象后您将停止引发事件。不要让他失望。
  • 那是肯定的@HansPassant。代码 sn-p 仅用于说明。在生产环境中,disposed 被跟踪,线程安全的委托实例也是如此。

标签: c# .net events delegates idisposable


【解决方案1】:

IDisposable 用于确定性地释放非托管资源。

无需删除事件处理程序。例如,如果您查看 Windows 窗体 FormUserControl 类,或 ASP.NET PageUserControl 类,所有这些都是 IDisposable,您会看到大量使用事件,而没有处置过程中的特殊处理。

【讨论】:

  • 特别是在 WinForms 方面,我经常遇到一些线程或计时器在处理表单后尝试更新 UI 的问题。所以我不确定如何将您的帖子解释为解决方案。
  • @RaheelKhan,您需要设计您的应用程序来避免这种情况:在您给出的示例中,当您的代码从另一个线程调用时,您应该在调用表单上的任何方法之前检查Form.IsDisposed。但这与事件处理程序无关。
  • @Joe:对不起,我不同意。未移除的事件订阅可能使对象永远保持活动状态,并可能导致难以发现的内存泄漏。使用弱事件模式有助于解决这个问题(请参阅我自己发布的答案)。
  • @JensH,如果你有静态事件处理程序,这是一个问题,应该完全避免或用弱事件模式代替。或者实例中的事件处理程序本身被静态引用,这相当于同一件事。但在一般情况下,包括 OP 示例,这不会成为问题。
  • @Joe:问题不仅在于静态事件处理程序,还存在于所有事件处理程序,其发布者的 GC 生命周期大大超过了订阅者的有用生命周期。我对 .net 的主要 不满之一是它使事件清理变得尴尬。 不应该让对象离开悬空的事件处理程序。它们通常是无害的,但细微的变化可能会将以前无害的悬空事件变成杀手。唉,事件清理缺乏语言支持意味着忽略它希望获得最好的结果通常比正确执行更具成本效益。
【解决方案2】:

您应该看看Weak Event Patterns,而不是“经典”事件订阅。

事件订阅可以使对象保持活动状态,即使这些引用是唯一剩下的引用并且被引用的对象本身已经超出范围。在这种情况下,被引用的对象将永远不会被 GarbageCollector 收集并一直保持活动状态,直到您的应用程序结束。

这会导致严重的内存泄漏。

如果您使用弱事件模式,您可以让 GabageCollector 更好地确定对象是否仍被引用,或者事件是否是唯一的引用。在这种情况下,对象会被收集,您的资源会被释放。

【讨论】:

  • 我不知道弱引用。它似乎比常规参考更复杂,但它可能是要走的路。谢谢。
  • @RaheelKhan:实现起来并没有太大的区别。但是一件很重要的事情,我们在工作中发现了一个艰难的过程,当我们的许多客户端机器突然崩溃并出现意外的OutOfMemoryExceptions... :-)
  • 是因为弱引用鼓励开发者偷懒吗?还是您的意思是内存不足异常是被放弃的硬引用的结果?
  • @RaheelKhan:这与懒惰无关。弱事件允许 GC 找到可能在代码中难以释放的废弃引用(考虑带有异常处理的异步场景)。在我上面的示例中,尽管尝试正确清理,但当时我们还不知道废弃引用的影响。所以OutOfMemoryExceptions 是由成千上万的废弃引用引起的。使用弱事件“简单地”修复了内存泄漏。
  • 我之前误解了你的意思,现在看看你的意思。例如,在处理大量数据行时,您可以很容易地遇到带有实体对象和 PropertyChanged 事件的那种场景。我知道弱引用是如何克服这一点的。
【解决方案3】:

这部分代码:

private ClassA A { get; set; }

public ClassB()
{
    this.A = new ClassA();
    this.A.OnProcessed += new ClassA.DelegateProcessed(this.ClassA_Processed);
}

表示你必须什么都不做

B 实例拥有一个A 实例,A 再次拥有对B 的引用(通过事件)。

B 变得无法访问时,A 也将被收集(GC 和循环引用)。

当“A”在 B 之前被释放(长)时,“A”也将被收集(方向性)。

A 上的 IDispoable 接口毫无意义。


关于实施:

 // class B
   this.A.OnProcessed += new ClassA.DelegateProcessed(this.ClassA_Processed);

 // in classA
   this.OnProcessed -= new ClassA.DelegateProcessed(this.ClassA_Processed);

这不起作用,2 个不同的this 表示它们是 2 个不同的方法。

【讨论】:

  • 我明白这一点。话虽如此,如果 B 的范围是应用程序范围的,它并不能确保 A 将在 B 之前很长时间被收集。我错过了什么吗?
  • 实际上 A 上的 IDisposable 是出于其他原因。但我也想确保 A 在内存中的寿命最小化。
  • 好的,但是你的代码不成立。只有当 A 持有(非)托管资源或占用大量内存时,Dispose() 才会有任何效果。在当前代码中this.A = null; 就足够了。
【解决方案4】:

正确编写的类应该在其IDisposable.Dispose 方法中取消订阅它已订阅的任何事件。如果订阅了事件的对象的 GC 生命周期与订阅的对象的有用生命周期相当(这是一种非常常见的情况),那么订阅是否被清理或悬空都无关紧要。不幸的是,如果A 在没有取消订阅B 的事件的情况下被放弃,并且某些东西保持对B 的长期引用(有意或无意),那么任何保持B 存活的东西也将保持存活@987654326 @ 以及 A 持有直接或间接引用的任何内容(包括具有来自 A 的活动事件订阅的对象)。很容易得到相互连接的对象的大森林,这些对象通常可以进行垃圾收集,但只要 任何 个对象都必须保持活动状态,所有需要。

太糟糕了事件订阅和取消订阅太尴尬了。如果有一个与事件关联的对象类型,一个要订阅各种事件的对象可以使用“事件管理器”对象来管理订阅(因此可以说类似MyEventManager.Subscribe(SomeObject.SomeEvent, someProc) 然后让MyEventManager.Dispose 取消订阅所有它已建立订阅的事件。不幸的是,没有像样的方法让一个方法接受一个事件作为参数,因此没有办法让一个通用类来管理传入的订阅。最好的办法可能是有一个 CleanupManager 类,它将接受一对委托并被调用,例如 `MyCleaner.Register(()=>{SomeObject.SomeEvent += someProc;}, ()=>{SomeObject.SomeEvent -= someProc() ;}) 但这似乎很尴尬。

【讨论】:

    猜你喜欢
    • 2019-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-11
    • 1970-01-01
    相关资源
    最近更新 更多