在IDiposable类的Dispose方法中从外部事件取消注册是一种好习惯吗?

我阅读了解释如何使用Dispose模式的优秀答案,以及它为何如此工作。

正确使用IDisposable接口

该帖明确指出您希望在两种不同的场景中使用Dispose模式:

  1. 摆脱非托管资源(因为我们必须)
  2. 摆脱托管资源(因为我们想要帮助)

我的问题是:

  • 当一个对象在其整个生命周期内订阅了一个外部事件时,在Dispose方法中从该事件中取消注册也是常见/好的做法吗? 你会为此目的实现IDisposable接口吗?

是的你应该。

这是向您的class级的消费者表明其具有必须发布的“资源”的最佳方式。 (即使事件订阅在技术上不是资源)

在许多(大多数?)情况下,在调用Dispose后很快就会有一个对象有资格进行垃圾回收。 例如,对于使用using语句实例化的IDisposable对象,这将始终为true:

 using(var myDisposableObject = ...) { ... } // myDisposableObject.Dispose() called here // myDisposableObject is no longer reachable and hence eligible for garbage collection here 

在这种情况下,我个人不会在一般情况下删除事件订阅时使代码混乱。

例如,ASP.NET PageUserControlIDisposable ,并且通常处理来自网页上其他控件的事件。 在处理PageUserControl时,不需要删除这些事件订阅,事实上我从未见过这样做的ASP.NET应用程序。

UPDATE

其他回答者建议您应该始终取消订阅IDisposable类的Dispose方法中的事件。

在一般情况下,我不同意这一点,尽管可能存在特定于应用程序的情况。

逻辑结论是,任何订阅事件的类都应该是IDisposable ,以便它可以确定性地取消订阅 – 我认为没有合理的理由说明为什么这个建议应该只适用于碰巧拥有非托管资源的类。 由于以下原因,我不认为这是一个很好的一般性建议:

  • 使类IDisposable正好可以取消订阅事件,这增加了类的用户的复杂性。

  • 取消订阅Dispose方法中的事件需要开发人员跟踪需要删除的事件订阅 – 有点脆弱,因为很容易错过(或者维护开发人员添加一个)。

  • 在类订阅来自长期发布者的事件的情况下,使用弱事件模式来确保订阅者的生命周期不受事件订阅的影响可能更合适。

  • 在许多情况下(例如,从其子控件订阅事件的ASP.NET Page类),发布者和订阅者的生命周期密切相关,因此无需取消订阅。

我更喜欢采用双管齐下的方法:

(1) UnregisterFromExternalEvents();的显式方法UnregisterFromExternalEvents();

(2)在Dispose()调用该方法。

这样,任何控制类实例的代码都可以显式取消注册,或者信任Dispose以正确处理和处理此类事务。

是的,将未注册的所有外部事件视为优秀做法,但由于事件的松散耦合性质,这并非极其必要。 它从事件生成器中删除订户对象的事件入口点引用,是的将是有帮助的。

对于取消订阅处理方法的部分也没关系。 Dispose方法的拇指规则是 – “Dispose方法应该以一种方式卸载资源,如果多次调用dispose它仍然可以工作,即你应该释放资源只处置一次。(这需要在处理之前进行检查资源)”

是的,这是一个非常好的主意。 事件发布者拥有对事件订阅者的引用,这将阻止订阅者被垃圾回收。 (请参阅事件处理程序是否阻止垃圾收集发生? )

此外,如果事件处理程序使用您要发布的资源,则事件处理程序(事件发布者将继续调用它们)可能会在资源释放后生成exception。

因此,重要的是发布资源之前从任何事件中取消注册,尤其是在使用异步或multithreading时,因为在某些情况下可能会在释放资源和从该事件取消注册之间引发事件。 。

以下代码演示了这一点:

 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ReleaseEvents { class Program { public static event EventHandler SomethingHappened; static void Main( string[] args ) { using ( var l_dependent = new Dependent() ) { SomethingHappened( null, EventArgs.Empty ); } // Just to prove the point, garbage collection // will not clean up the dependent object, even // though it has been disposed. GC.Collect(); try { // This call will cause the disposed object // (which is still registered to the event) // to throw an exception. SomethingHappened( null, EventArgs.Empty ); } catch ( InvalidOperationException e ) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine( e.ToString() ); } Console.ReadKey( true ); } } class Dependent : IDisposable { private object _resource; public Dependent() { Program.SomethingHappened += Program_SomethingHappened; _resource = new object(); } private void Program_SomethingHappened( object sender, EventArgs e ) { if ( _resource == null ) throw new InvalidOperationException( "Resource cannot be null!" ); Console.WriteLine( "SomethingHappened processed successfully!" ); } public void Dispose() { _resource = null; } } } 

第二次引发SomethingHappened事件时, Dependent类将抛出InvalidOperationException。 您必须取消注册该事件以防止这种情况发生:

  class Dependent : IDisposable { // ... public void Dispose() { Program.SomethingHappened -= Program_SomethingHappened; _resource = null; } } 

当我第一次尝试实现MVVM架构时,我遇到了这个问题。 当我在ViewModels之间切换时,我只是发布了我认为唯一的参考(ActiveViewModel属性)。 我没有意识到我的ViewModel订阅的事件是将它保存在内存中。 随着应用程序运行时间变长,它变得越来越慢。 最终我意识到我认为我发布的ViewModel实际上正在继续处理事件(昂贵)。 我必须明确释放事件处理程序来解决这个问题。

感谢这个问题,我将把我的课程写成:

 class Foo : IDisposable { public event EventHandler MyEvent; ///  /// When disposing unsubscibe from all events ///  public void Dispose() { if (MyEvent != null) { foreach (Delegate del in MyEvent.GetInvocationList()) MyEvent -= (del as EventHandler); } // do the same for any other events in the class // .... } }