GOTCHA #40 Implementing IDisposable isn't enoughLet's review key points from Gotcha #35, Gotcha #38, and Gotcha #39:
For these reasons, it is better to use Dispose() instead of relying on Finalize(). That sounds good, except for one problem. Unfortunately, the author of a class has no way to force the user of an object to invoke the Dispose() method. It requires discipline on the part of the programmer to call Dispose() at the appropriate location in the code. What if he forgets to? You want the resources to be cleaned up anyway; doing it in Finalize() is better than not doing it at all. You know that Finalize() will be called eventually, you just don't know when. So what code should you put in Finalize()? Remember that you should not touch any managed resources (Gotcha #36, "Releasing managed resources in Finalize( ) can wreak havoc"). But you do need to clean up unmanaged resources. What code do you write in Dispose()? If you call GC.SuppressFinalize() in Dispose() (as you should), you won't be able to rely on Finalize() to clean up unmanaged resources; Dispose() will have to take care of them. In addition, since Dispose() (unlike Finalize()) is not executed in the middle of a garbage-collection sweep, it can safely release other managed objects. So within Dispose() you take care of cleaning up both managed and unmanaged resources. How do you free unmanaged resources in Dispose() and Finalize() without duplicating code? In Gotcha #38, "Depending on Finalize() can tie up critical resources," these two methods made identical calls to ReleaseComObject() (see Example 5-5). The Dispose design pattern will help properly refactor Dispose() and Finalize(). (Refer to "Dispose Design Pattern" in the section "on the web" in the Appendix.) This pattern stipulates that both Finalize() and IDisposable.Dispose() delegate to a third protected virtual/Overridable method. According to Microsoft's specification, this method should be called Dispose() as well, and should take one bool/Boolean parameter, which Microsoft calls disposing. This indicates whether the method has been called by IDisposable.Dispose() (true) or Finalize() (false). This protected Dispose() method does all resource cleanup. Whether the disposing argument is true or false, it cleans up unmanaged resources. But only if this argument is TRue, indicating that the object is still accessible, does it attend to managed resources. The Dispose(bool disposing) method is protected because you don't want users of your class to call it directly, and you need to override it in any derived classes.
With this pattern, only classes at the top of your class hierarchies need to implement Finalize() and IDisposable.Dispose(). Those two methods call Dispose(false) and Dispose(true), respectively. Derived classes just implement the protected version of Dispose(). Because this method is defined as virtual/Overridable in the base class, and override/Overrides in all derived classes, the correct derived implementation will execute.
To complete the pattern, the derived class's Dispose() must invoke its base class's Dispose(); i.e., the protected version with the disposing parameter. Example 5-7 and Example 5-8 illustrate the Dispose Design Pattern. Example 5-7. Dispose design pattern (C#)C# (DisposeFinalize) //Base.cs using System; public class Base : IDisposable { private bool disposed = false; private readonly int id; public Base(int theID) { id = theID; } protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { Console.WriteLine( "Base Cleaning up managed resources on {0}", id); // Code to clean up managed resources } Console.WriteLine( "Base Cleaning up unmanaged resources on {0}", id); // Code to clean up unmanaged resources } disposed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } ~Base() { Console.WriteLine("*** Finalize called on Base {0}", id); Dispose(false); } } //Derived.cs using System; namespace DisposePattern { public class Derived : Base { private bool disposed = false; public Derived(int theID) : base(theID) {} protected override void Dispose(bool disposing) { if (!disposed) { try { if (disposing) { Console.WriteLine( "Derived Cleaning up managed resources"); // Code to clean up managed resources } Console.WriteLine( "Derived Cleaning up unmanaged resources"); // Code to clean up unmanaged resources } finally { base.Dispose(disposing); } } disposed = true; } } } //Test.cs using System; namespace DisposePattern { class Test { [STAThread] static void Main(string[] args) { Derived object1 = new Derived(1); Derived object2 = new Derived(2); object1.Dispose(); } } } Example 5-8. Dispose design pattern (VB.NET)VB.NET (DisposeFinalize) 'Base.vb Public Class Base Implements IDisposable Private disposed As Boolean = False Private ReadOnly id As Integer Public Sub New(ByVal theID As Integer) id = theID End Sub Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not disposed Then If disposing Then Console.WriteLine( _ "Base Cleaning up managed resources on {0}", _ id) ' Code to cleanup managed resources End If Console.WriteLine( _ "Base Cleaning up unmanaged resources on {0}", id) ' Code to cleanup unmanaged resources End If disposed = True End Sub Public Sub Dispose() Implements System.IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub Protected Overrides Sub Finalize() MyBase.Finalize() Console.WriteLine("*** Finalize called on Base {0}", id) Dispose(False) End Sub End Class 'Derived.vb Public Class Derived Inherits Base Private disposed As Boolean = False Public Sub New(ByVal theID As Integer) MyBase.New(theID) End Sub Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) Try If Not disposed Then If disposing Then Console.WriteLine( _ "Derived Cleaning up managed resources") ' Code to cleanup managed resources End If Console.WriteLine( _ "Derived Cleaning up unmanaged resources") ' Code to cleanup unmanaged resources End If disposed = True Finally MyBase.Dispose(disposing) End Try End Sub End Class 'Test.vb Module Module1 Sub Main() Dim object1 As New Derived(1) Dim object2 As New Derived(2) object1.Dispose() End Sub End Module Take a look at the Base class. It implements IDisposable, so it must provide a Dispose() method with no parameter. This method invokes the Dispose(bool disposing) method with true as its argument. The Finalize() method invokes Dispose(bool disposing) as well, but passes false instead of true. The Dispose(bool disposing) method cleans up unmanaged resources regardless of the parameter's value. It only cleans up managed resources if disposing is TRue, that is, only if it's been called by the no-parameter Dispose() method. If invoked by the Finalize() method, it does not touch any managed resources. See Gotcha #36, "Releasing managed resources in Finalize( ) can wreak havoc." In the Derived class, you do not have to implement the Finalize() method or the no-parameter Dispose(). Instead, you simply override the Dispose() method that takes the bool/Boolean parameter. First you clean up managed resources, but only if the disposing parameter is true. Next, you take care of unmanaged resources. Finally, you call the corresponding Dispose() method of the Base class. The output from the above program is shown in Figure 5-7. Figure 5-7. Output from Example 5-7From the output, you can see that object1 is cleaned up using the Dispose() method and that its Finalize() is not called. Both managed and unmanaged resources are cleaned up properly here. On the other hand, object2 is cleaned up in the Finalize() method. Only the unmanaged resources are deleted in this case, as expected. With this code, you still need to be concerned about a couple of problems. First, you may have to make some adjustments to the Dispose(bool) method for thread-safety in a multithreaded application. Second, it is possible for a user to call the Dispose() method on an object more than once, and to call other methods on an object after the call to Dispose(). In your methods, you need to check if the object has been disposed. If it has, you should raise an ObjectDisposedException. As a final illustration of the Dispose Design Pattern, let's revisit the Wrapper class from Gotcha #38, "Depending on Finalize() can tie up critical resources" (see Example 5-5). In the previous version, both Finalize() and Dispose() call ReleaseComObject(). By moving this call to Dispose(bool disposing), you remove the duplication. The Wrapper class in this example has no managed resources and no base class below System.Object. Therefore, it does not call base.Dispose(). Example 5-9 shows the refactored implementation. Example 5-9. Refactored Wrapper ClassC# (RefactoredWrapper) //Wrapper.cs using System; using ACOMCompLib; namespace FinalizePeril { public class Wrapper : IDisposable { IMyComp comp = new MyCompClass(); bool disposed = false; public int doSomething() { if (disposed) { throw new ObjectDisposedException(null); } int result; comp.doSomething(out result); return result; } protected virtual void Dispose(bool disposing) { if (!disposed) { // No managed resources to clean up // Clean up unmanaged resources regardless of who called us System.Runtime.InteropServices.Marshal.ReleaseComObject(comp); GC.SuppressFinalize(this); disposed = true; } } ~Wrapper() { Dispose(false); } #region IDisposable Members public void Dispose() { Dispose(true); } #endregion } } VB.NET (RefactoredWrapper) 'Wrapper.vb Imports ACOMCompLib Public Class Wrapper Implements IDisposable Dim comp As IMyComp = New MyCompClass Private disposed As Boolean = False Public Function doSomething() As Integer If disposed = True Then Throw New ObjectDisposedException(Nothing) End If Dim result As Integer comp.doSomething(result) Return result End Function Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not disposed Then ' No managed resources to clean up ' Clean up unmanaged resources regardless of who called us System.Runtime.InteropServices.Marshal.ReleaseComObject(comp) GC.SuppressFinalize(Me) disposed = True End If End Sub Public Sub Dispose() Implements System.IDisposable.Dispose Dispose(True) End Sub Protected Overrides Sub Finalize() Try Dispose(False) Finally MyBase.Finalize() End Try End Sub End Class IN A NUTSHELLFollow the Dispose design pattern (with adjustments for thread-safety if needed), because it provides an effective way to clean up both managed and unmanaged resources. SEE ALSOGotcha #35, "Writing Finalize( ) is rarely a good idea," Gotcha #36, "Releasing managed resources in Finalize( ) can wreak havoc," Gotcha #37, "Rules to invoke base.Finalize( ) are not consistent," Gotcha #38, "Depending on Finalize() can tie up critical resources," Gotcha #39, "Using Finalize() on disposed objects is costly," and Gotcha #41, "Using the Dispose Design Pattern doesn't guarantee cleanup." |