In which case should my class implement the IDisposable interface? Prompt the correct implementation. What are unmanaged resources, and how should they be closed?

1 answer 1

What is it all about?

Objects in .NET are destroyed by their cunning rules. Not like in C ++, where an object is destroyed as soon as it goes out of scope. In .NET, garbage collection is implemented: if the object is not referenced, then it is considered useless, and the “garbage collector” finds it and eats it. And not immediately, but when he pleases, maybe even never at all. [In fact, if the object is a link, but from another object that is not needed by anyone, then this link does not seem to be considered.]

For example, if you nullify a link to an object, it will not lead to its immediate destruction. (This will not necessarily lead to its destruction even later: there may be other links to this object.)

This can lead to unpleasant effects. For example, if we in C ++ open a file in the object's constructor, then we can close it in the destructor. At the same time, we know for sure when the file will be closed: at the end of the block in which the object is declared. If we open the file in the C # class constructor, and try to close it in the destructor, it will turn out bad: the destructor is called only when the garbage collector clears the object, that is, it is not clear when, and maybe even never. This means that if we try to open the same file again in another part of the program, we will not succeed: the file may still be opened by the old object.

It turns out that the destructor in C # is practically useless, and you will have to use it very rarely. (He, by the way, is officially called the finalizer .)

What is the way out of this situation? There is a way out, but it requires attention.

You can declare a method that can be called when an object should "die." This is done using the IDisposable interface, which has one Dispose() method. In this method, and should be "cleanup".

But this method will not be called automatically . This method should call you anyway, the system will not do it for you. For the IDisposable system, just another interface, it does not have any special significance (for example, the destructor is not aware of it at all). There is also a convenient using construct that will call the Dispose method for you at the end of the block:

 using (StreamReader r = new StreamReader(path)) { Console.WriteLine(r.ReadLine()); } 

it's almost the same as

 StreamReader r = new StreamReader(path); Console.WriteLine(r.ReadLine()); r.Dispose(); 

(but it works correctly in the case of exceptions and the like).

Why such a method, if there is a destructor? As we have already found out, the destructor is called incomprehensibly when, or never at all. It is impossible to call the destructor at the right moment. But you can call a method, such as Dispose , at the right time.

When is it necessary?

Let's start with a simple example. Let you have a field in the class (well, or a property) that implements IDisposable . In this case, your class must also implement IDisposable in order to call Dispose during its Dispose for the internal object as well.

Another case where you almost always need to implement IDisposable is if your class works with WinAPI, and has a handle to an external object. For example, you open a file through WinAPI using P / Invoke. In this case, you need to close it in time, and to do this, implement IDisposable .

Let's see what this means in general. In our class there is some kind of thing for which we are responsible, and which must be nailed at the end of the existence of the class. Such a thing is usually called a resource .

Resources are divided into managed (those that are essentially .NET objects) and unmanaged (they are usually system handles and are stored in an IntPtr , but in principle can be any object outside a given .NET runtime, or even just a purely logical entity like rights to display notifications to the user). A managed resource can also contain unmanaged resources.

If your class contains a resource (for example, a field or property of type IDisposable or an unmanaged WinAPI object) you must (most likely) implement IDisposable .

Closing a managed resource almost always comes down to calling Dispose , Dispose this resource itself must implement IDisposable . Closing an unmanaged resource is done in a way specific to the type of this resource.

If you use an unmanaged resource, turn it into a manageable resource by packing in SafeHandle (see below). [Unless you have a very good reason for not doing this. No, laziness is not considered a valid reason.]

If you store a reference to other objects in your class, but these objects do not implement IDisposable , then you most likely do not need to implement IDisposable yourself. Just memory is not a resource that needs to be freed : the garbage collector is responsible for you. Since you cannot call Dispose on the sub-objects, they will be released when the garbage collector “eats” them. Since after Dispose your object is usually no longer needed, then he himself will soon be unavailable, and references from him to sub-objects will not be important for the garbage collector, so zeroing the links will not give anything.

That is, if your fields are ordinary objects, not IDisposable (for example, strings), then you most likely do not need to implement IDisposable for your class. If any one of your subobjects implements IDisposable , then you also most likely need to implement IDisposable .

Implementing IDisposable for the case when you have both managed and unmanaged resources in your class is complex and contains many fine points. Therefore, Microsoft strongly recommends that you do not try to do this, but wrap an unmanaged resource in SafeHandle (or another object whose purpose is a wrapper for a resource), and work only with managed resources at the level of your class.

How to implement IDisposable correctly?

If you have unmanaged resources in your class, make a manageable wrapper for them, as explained below.

For the case when your class has no descendants, you just have to release resources in Dispose . In the event that a client forgets to call Dispose , the subobjects of your class will be eaten by the garbage collector, and they (or their subobjects) will get a finalizer that will free up resources. Your class does not need a finalizer at all.

Here is an example implementation skeleton (borrowed from this answer ):

 sealed class C : IDisposable { SomeResource1 resource1; SomeResource2 resource2; // тут могут быть ещё ресурсы bool isDisposed = false; public C() { try { resource1 = AllocateResource1(); resource2 = AllocateResource2(); } catch { Dispose(); throw; } } public void Use() { if (isDisposed) // использовать удалённый объект -- ошибка, её лучше проверять throw new ObjectDisposedException("Use called on disposed C"); // ... } public void Dispose() { // мы уже умерли? валим отсюда if (isDisposed) return; // Dispose имеет право быть вызван много раз // освободим ресурсы if (resource2 != null) { resource2.Dispose(); resource2 = null; } if (resource1 != null) { resource1.Dispose(); resource1 = null; } // и запомним, что мы уже умерли isDisposed = true; } } 

Why do we need try / catch in the constructor? If obtaining a resource can fail or throw an exception, it makes sense to release the resources immediately, because in this case the client will not be called Dispose (he will not receive a reference to the object). If the code in the constructor is guaranteed cannot throw an exception, the pattern can be simplified:

 sealed class C : IDisposable { SomeResource1 resource1; SomeResource2 resource2; bool isDisposed = false; public C() { resource1 = AllocateResource1(); resource2 = AllocateResource2(); } public void Use() { if (isDisposed) // использовать удалённый объект -- ошибка, её лучше проверять throw new ObjectDisposedException("Use called on disposed C"); // ... } public void Dispose() { if (isDisposed) return; resource2.Dispose(); resource1.Dispose(); isDisposed = true; } } 

For the case of a class hierarchy, the Dispose method in the base class must be declared virtual, and do not forget to call base.Dispose() at the end of the generated classes:

 class C : IDisposable { // ... public virtual void Dispose() { if (isDisposed) return; if (resource != null) { resource.Dispose(); resource = null; } isDisposed = true; } } class C2 : C { // ... public override void Dispose() { if (isDisposed) return; if (resource2 != null) { resource2.Dispose(); resource2 = null; } isDisposed = true; base.Dispose(); } } 

Dispose(bool disposing) , recommended by FxCop, are not needed for finalizers and patterns, since our classes do not contain unmanaged resources. So a warning about this can be ignored.

How to create a managed wrapper?

So, we have an unmanaged resource (that is, a resource that is not reducible to a .NET object). We need to build an IDisposable wrapper for it.

To begin with, if our resource is a handle, then most likely you already have a descendant of SafeHandle defined in the framework that is suitable for your resource. Take a look at the Microsoft.Win32.SafeHandles namespace. In particular, you can use

These classes are ready-made wrappers, you can use them in P / Invoke definitions. Or if one of the ready-made classes does not quite suit you, you can inherit from SafeHandle and get your wrapper. An example from here demonstrates both techniques:

 using System.Runtime.InteropServices.ComTypes; using Microsoft.Win32.SafeHandles; class FindHandle : SafeHandleZeroOrMinusOneIsInvalid { private FindHandle() : base(true) { } protected override bool ReleaseHandle() { return FindClose(this); } } [DllImport("kernel32.dll")] static extern bool FindClose(FindHandle handle); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] struct DATA // WIN32_FIND_DATA { public FileAttributes FileAttributes; public FILETIME CreationTime, LastAccessTime, LastWriteTime; public uint FileSizeHigh, FileSizeLow; public uint Reserved0, Reserved1; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string FileName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] public string AlternateFileName; } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern FindHandle FindFirstFileEx(string name, int i, out DATA data, int so, IntPtr sf, int f); [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern bool FindNextFile(FindHandle h, out DATA data); 

If you need to create your own wrapper, and ready-made base classes do not fit, this is the backbone:

 class CustomResourceHolder : IDisposable { IntPtr resource; bool isDisposed = false; public CustomResourceHolder() { resource = AllocateUnmanagedResource(); // эта строчка нужна в конце конструктора, иначе финализатор может начать // есть объект до окончания работы конструктора! GC.KeepAlive(this); } public IntPtr GetHandleDangerous() { return resource; } public void Dispose() { DoDispose(); // эта строка гарантирует, что объект будет считаться достижимым // до конца DoDispose, и что в случае Dispose финализатор вызван не будет GC.SuppressFinalize(this); } ~CustomResourceHolder() { DoDispose(); } void DoDispose() { if (isDisposed) return; // идемпотентность Dispose // в любом случае освободим ресурс // нам нужна понадобиться проверка того, а был ли реально аллоцирован // ресурс (например, его выделение могло бросить исключение) if (resource реально был выделен) FreeUnmanagedResource(resource); // и запомним, что мы уже умерли -- это должно быть последней строкой isDisposed = true; } } 

A footnote for connoisseurs : why do GC.KeepAlive(this) need GC.KeepAlive(this) at the end of the constructor? We need to ensure that the finalizer does not start running until the end of the constructor. (He can! See here [section “ Myth: An object being finalized was fully constructed ”] and here ). The body of the constructor in real code can be more complicated, and guarantees that the last in the constructor will be the call to this , and not work with the obtained resource, is quite difficult and requires separate efforts. (The optimizer also introduces its share of complexity, which can rearrange pieces of code.)

Using GC.KeepAlive(this) at the end of the constructor allows you to get rid of these problems in the simplest way.


Thank you so much @PashaPash, @Stack, @Discord, @Pavel Mayorov and @ i-one, which with their constructive criticism and recommendations helped a lot to improve the response.

  • "unmanaged (they are usually handles of the system and stored in IntPtr)" - more precisely: resources are unmanaged, if they are / are stored outside the .NET runtime. for example, the window handle is IntPtr, but the window itself is in the Windows window subsystem, see here - Stack
  • with classes C and C2 : C it is not clear isDisposed one or everyone has his own. - Stack
  • @Stack: Yeah, everyone has their own. Although in principle it is not important, it is possible and generally protected. - VladD
  • @Stack: Yes, or just a speculative construction, such as "the right to show ads" ( stackoverflow.com/a/10316452/276994 ). (This was the first comment.) - VladD