📜 ⬆️ ⬇️

How to apply IDisposable and finalizers: 3 simple rules

From translator


After the story about memory leakage and the correct implementation of events, I post another translation of an article I liked about memory management. I saw several different implementations of the Dispose pattern, sometimes they even contradicted each other. In this article, the author presented a good and clear explanation of when to implement the IDisposable interface, when the finalizers, and when - all together.

How to apply IDisposable and finalizers: 3 simple rules


Microsoft's documentation on using IDisposable is rather confusing. In fact, it is simplified to three simple rules.

Rule one: do not apply (until it is really needed)


By implementing the IDisposable interface, you do not create a destructor. Remember that in a .NET environment, there is a garbage collector that works well enough not to assign null to multiple variables.

There are only two situations when it is necessary to implement IDisposable. Look at the class and determine if you need this interface:

Please note that resources should only be released by the classes to which these resources belong. In particular, a class may have a link to a shared resource — in this case, you should not release it, since other classes may continue to use this resource.
')
Here is a sample code that many novice programmers write:

//    IDisposable. public sealed class ErrorList : IDisposable { private string category; private List<string> errors; public ErrorList(string category) { this.category = category; this.errors = new List<string>(); } // ( / ) //  public void Dispose() { if (this.errors != null) { this.errors.Clear(); this.errors = null; } } } 

Some programmers (especially those who have previously worked with C ++) go even further and add a finalizer:

 //       IDisposable. public sealed class ErrorList : IDisposable { private string category; private List<string> errors; public ErrorList(string category) { this.category = category; this.errors = new List<string>(); } // ( / ) //   public void Dispose() { if (this.errors != null) { this.errors.Clear(); this.errors = null; } } ~ErrorList() { //  !</font> //            !</font> this.Dispose(); } } 

An example of the correct implementation of IDisposable for the described class:

 //     IDisposable. public sealed class ErrorList { private string category; private List<string> errors; public ErrorList(string category) { this.category = category; this.errors = new List<string>(); } } 

That's right. Proper use of the IDisposable interface for this class - do not use it! When the ErrorList instance becomes inaccessible, the garbage collector automatically frees the memory it uses.

Remember these two criteria for using IDisposable - the class must own unmanaged or managed resources. You can go through the points:

1. Does the ErrorList class own unmanaged resources? No, not owned.
2. Does the ErrorList class own managed resources? Remember, “managed resources” are classes that implement IDisposable. Check each class member:
1. Does string class implement IDisposable? No, it does not.
2. Does the List class implement IDisposable? No, it does not.
3. If no member implements IDisposable, the ErrorList class does not own managed resources.
3. Since ErrorList does not own any managed or unmanaged resources, it does not require the implementation of the IDisposable interface.

Rule two: for a class that owns managed resources, implement IDisposable (but not the finalizer)


The IDisposable interface has only one method: Dispose. When implementing this method, you have to fulfill one important obligation: even a multiple call to Dispose should occur without errors.

The implementation of the Dispose method implies that: this method is not called from the finalizer thread, the object instance has not yet been collected by the garbage collector, and the object constructor has successfully completed. These assumptions make secure access to managed resources.

Placing a finalizer in a class that owns only managed resources can lead to errors. This sample code may cause an exception in the finalizer thread and will break the application:

 //       . public sealed class SingleApplicationInstance { private Mutex namedMutex; private bool namedMutexCreatedNew; public SingleApplicationInstance(string applicationName) { this.namedMutex = new Mutex(false, applicationName, out namedMutexCreatedNew); } public bool AlreadyExisted { get { return !this.namedMutexCreatedNew; } } ~SingleApplicationInstance() { // , , !!! this.namedMutex.Close(); } } 

It does not matter if SingleApplicationInstance implements the IDisposable interface, the mere fact of accessing managed objects in the finalizer is the right path to errors.

Here is an example of a class in which there is no finalizer, and the IDisposable interface is implemented in the right, but in a too complicated way:

 //     IDisposable. public sealed class SingleApplicationInstance : IDisposable { private Mutex namedMutex; private bool namedMutexCreatedNew; public SingleApplicationInstance(string applicationName) { this.namedMutex = new Mutex(false, applicationName, out namedMutexCreatedNew); } public bool AlreadyExisted { get { return !this.namedMutexCreatedNew; } } //   public void Dispose() { if (namedMutex != null) { namedMutex.Close(); namedMutex = null; } } } 

If a class owns managed resources, it can in turn call the Dispose method on them. No additional code needed. Remember that some classes rename "Dispose" to "Close", so the implementation of the Dispose method can consist solely of calls to the Dispose and Close methods.

Equal and simpler implementation:

 //    IDisposable. public sealed class SingleApplicationInstance : IDisposable { private Mutex namedMutex; private bool namedMutexCreatedNew; public SingleApplicationInstance(string applicationName) { this.namedMutex = new Mutex(false, applicationName, out namedMutexCreatedNew); } public bool AlreadyExisted { get { return !this.namedMutexCreatedNew; } } public void Dispose() { namedMutex.Close(); } } 

This implementation of the Dispose method is completely safe. It can be called as many times as necessary, since each implementation of IDisposable child resources is in turn safe. This transitive property should be used to write similar simple implementations of the Dispose method.

Rule three: for a class that owns unmanaged resources, implement IDisposable and finalizer


A class that owns one unmanaged resource should not be responsible for something else. His only obligation is to close this resource.

Classes should not be responsible for several unmanaged resources. It is quite difficult to properly release one resource, it is even more difficult to write a class that contains several unmanaged resources.

Classes should not be responsible for managed and unmanaged resources together. It is possible to write such a class, but it is very difficult to do it correctly. Believe me; better not try. Even if there are no mistakes in the class, his accompaniment turns into a nightmare. By the release of .NET 2.0, Microsoft rewrote many classes from the BCL (base class library) - divided them into owning unmanaged and managed resources.

Note: The presence of such complex official documentation on IDisposable is due to the fact that Microsoft believes that your class will contain both types of resources. This is a retention of .NET 1.0, left for backwards compatibility. Even classes written by Microsoft do not follow this old pattern (they were modified in .NET 2.0 using the pattern described in this article). FxCop will say that you need to “correctly” implement IDisposable (that is, use the old template). Do not listen to him - FxCop is mistaken.

The class should look similar:

 //    IDisposable. //       SafeHandle. public sealed class WindowStationHandle : IDisposable { public WindowStationHandle(IntPtr handle) { this.Handle = handle; } public WindowStationHandle() : this(IntPtr.Zero) { } public bool IsInvalid { get { return (this.Handle == IntPtr.Zero); } } public IntPtr Handle { get; set; } private void CloseHandle() { //   ,    if (this.IsInvalid) { return; } //  ,   if (!NativeMethods.CloseWindowStation(this.Handle)) { Trace.WriteLine("CloseWindowStation: " + new Win32Exception().Message); } //     this.Handle = IntPtr.Zero; } public void Dispose() { this.CloseHandle(); GC.SuppressFinalize(this); } ~WindowStationHandle() { this.CloseHandle(); } } internal static partial class NativeMethods { [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool CloseWindowStation(IntPtr hWinSta); } 

At the end of the Dispose method is a call to GC.SuppressFinalize (this) . This ensures that the object finalizer will not be called.

If Dispose is not called explicitly, then the finalizer will eventually work and close the handle.

The CloseHandle method first checks if the handle is null. Then it closes it, without throwing possible exceptions: CloseHandle can be called from the finalizer, and throwing an exception will stop the process. The CloseHandle method ends with resetting the handle. So it can be called as many times as necessary. This, in turn, makes it safe to call Dispose multiple times. Handle checks could be placed in Dispose, but placing this check in CloseHandle allows you to pass null handles to the constructor and assign them to the Handle property.

The reason SuppressFinalize is called after CloseHandle is that if an error occurs in Dispose when closing the handle, the finalizer will still be called. This reason was discussed in detail in the blog by Joe Duffy ( also a very good article, by the way - note of the translator ), although it is a rather weak argument. The difference would exist only if the CloseHandle method, when called from the finalizer, closed the handle in a different way. So, of course, you can, but not recommended.

Important! The WindowStationHandle class does not contain a window layout handle and does not know anything about creating or opening a window layout. These functions (as well as others related to windows) are a task of another class (probably “WindowStation”). It helps to create correct implementations, since each finalizer must be executed even on objects with constructors that are not completely exhausted due to the ejection of the exception. In practice, this is difficult to do, and this is another reason why the wrapper class should be divided into the class responsible for closing the handle and the wrapper class itself.

Note: the above solution is the simplest and has its drawbacks. For example, if a thread ends immediately after performing the resource allocation function, then this resource may leak. If you are packing an IntPtr handle, it is best to inherit from the SafeHandle class. If you need to go further and maintain a reliable release of resources, then everything quickly becomes very confusing ( one more good article - note. )!

Source: https://habr.com/ru/post/89720/


All Articles