“You shouldn’t follow some idiom just because everything is done this way or so it’s written somewhere”
Thoughts of the author of the article while reading and refactoring someone else's codeIt will not be a secret to anyone that the .NET platform supports automatic memory management. This means that if you create an object using the
new keyword, then you will not have to take care of it yourself. The garbage collector will determine the “reachability” of the object, and if there are no root links to the object, it will be released. However, as soon as it comes to resources, such as a socket, an unmanaged memory buffer, an operating system descriptor, etc., the garbage collector, by and large, washes his hands and the entire headache of working with such resources falls on the developer’s shoulders.
')
But what about the finalizers? - you ask. Well, yes, there is such a thing, finalizers are really meant for freeing resources, but the problem is that the time of their call is not deterministic, which means that no one knows when they will be called and whether they will be called at all. And the order of calling finalizers is not defined, so when you call the finalizer, some “parts” of your object can already be “destroyed”, since their finalizers have already been called. In general, finalizers - they exist, but this is more like a "safety cable", rather than a normal resource management tool.
Idiom raii
In C ++, in which there are no built-in tools for automatic memory management in addition to smart pointers, the pattern (or idiom) has long been actively used for the timely release of resources (*). This idiom is called Resource Acquisition Is Initialization (RAII - Resource Acquisition Is Initialization) and is as follows. The resource is captured in the constructor and released in the destructor, and since destructors are called automatically, no additional effort is needed to manage the resources.
Not surprisingly, the same idea of deterministic resource management pumped into other more "smart" and "managed" environments, such as .NET or Java (**) as an
IDisposable interface (in C #) and a
dispose method (in Java) . But since these environments are smarter compared to the old C ++, and the main problems associated with memory management are solved in them, this idiom moved not too well. No, do not misunderstand me, it moved quite successfully, but for this you need to use the
using block (for C #) or
try - with - resources statement (in Java 7), if you “forget” to use them, then from your deterministic release of resources will not remain a trace.
// using
using ( FileStream file = File .OpenRead( "foo.txt" ))
{
//
if (someCondition) return ;
// using
}
// , - using?
FileStream file2 = File .OpenRead( "foo.txt" );
* This source code was highlighted with Source Code Highlighter .
However, this is not the only difficulty that arises when working with resources in .NET. As we will see shortly, using the usual method to free up resources has some other problems. Since the
Dispose method frees resources, the finalizer call is no longer needed and it needs to be canceled; in addition, the
Dispose method destroys the class invariant, which allows the user to obtain a destroyed or partially destroyed object. And this requires additional checks both in the
Dispose method and in all public methods of the class.
All this led to the fact that the relatively simple idiom RAII resulted in a .NET platform in a pattern that is called the “Dispose pattern”. However, before proceeding to its consideration, let's consider two types of resources that exist on the .NET platform: managed and unmanaged resources.
Managed and Unmanaged Resources
There are two types of resources in .NET: managed and unmanaged. Moreover, it is quite simple to distinguish them: only “raw” resources, such as
IntPtr , raw socket descriptors or something like that, belong to unmanaged resources; if, using the RAII idiom, this resource was packed into an object that captures it in the constructor and releases it in the
Dispose method, then such a resource is already managed. In essence, managed resources are “smart shells” for unmanaged resources, to free them you don’t need to call some ingenious functions, but simply call
Dispose on the
IDisposable interface.
class NativeResourceWrapper : IDisposable
{
// IntPtr –
private IntPtr nativeResourceHandle;
public NativeResourceWrapper()
{
// «»
nativeResourceHandle = AcquireNativeResource();
}
public void Dispose()
{
// , , -
//
ReleaseNativeResource(nativeResourceHandle);
}
// ,
~NativeResourceWrapper() {...}
}
* This source code was highlighted with Source Code Highlighter .
Thus, any object can own two types of resources: it can directly contain an unmanaged resource (for example
, IntPtr ) or it can contain a link to a managed resource (for example,
NativeResourceWrapper ), and in both cases an object containing one of these resources, itself becomes a managed resource. This may not seem very important, but it is very important to understand the difference between the two types of resources, since it is necessary to work with them in different ways.
Dispose pattern
So, we know that an object can own two types of resources:
managed and
unmanaged ; as well as the fact that we have two ways to free resources:
deterministic , using the
Dispose method and
non-deterministic , using the finalizer (***). And now let's see how to live with all this good and, most importantly, how to release this good.
The idea behind the Dispose pattern is as follows: let's put the entire logic of resource release into a separate method, and we will call it from the
Dispose method and from the finalizer, while adding a checkbox to tell us who caused this method. Since this simple idea contains a fairly large number of details, let's describe the Dispose pattern by items.
1. The class containing managed or unmanaged resources implements the
IDisposable interface
class Boo : IDisposable { ... }
* This source code was highlighted with Source Code Highlighter .
2. The class contains the
Dispose method
( bool disposing ) , which does all the work of freeing resources; The
disposing parameter indicates whether this method is called from the
Dispose method or from the finalizer. This method must be
protected virtual for non-sealed classes and
private for sealed classes.
// -sealed
protected virtual void Dispose( bool disposing) {}
// sealed
private void Dispose( bool disposing) {}
* This source code was highlighted with Source Code Highlighter .
3. The Dispose method is always implemented as follows:
Dispose ( true ) is called first, and then the
GC method call can follow
. SuppressFinalize () , which prevents the finalizer from being called.
public void Dispose()
{
Dispose( true /*called by user directly*/ );
GC.SuppressFinalize( this );
}
* This source code was highlighted with Source Code Highlighter .
GC method
. SuppressFinalize () , first, should be called after calling
Dispose ( true ) , and not before it, because if
Dispose ( true ) “falls” with an exception, then the finalizer will not be canceled. Secondly,
GC . SuppressFinalize () must be called even for classes that do not contain finalizers, since a finalizer may appear to its successor (that is, we must call the
GC method
. SuppressFinalize () in all non-sealed classes).
4. The
Dispose method
( bool disposing ) contains two parts: (1) if this method is called from
Dispose (i.e. the
disposing parameter is
true ), then we release the managed and unmanaged resources and (2) if the method is called from the finalizer garbage collection time (
disposing parameter is
false ), then we release only unmanaged resources.
void Dispose( bool disposing)
{
if (disposing)
{
//
}
//
}
* This source code was highlighted with Source Code Highlighter .
5. (OPTIONAL) A class may contain a finalizer and call
Dispose ( bool disposing ) from it, passing
false as a parameter.
~Boo()
{
Dispose( false /*not called by user directly*/ );
}
* This source code was highlighted with Source Code Highlighter .
Do not forget that the finalizer can be called even for partially constructed objects if the constructor of this class throws an exception. So the cleanup code of unmanaged resources must take into account that resources have not yet been captured (****).
6. (OPTIONAL) The class may contain the
bool _ disposed field , which indicates that object resources have already been released. Disposable classes should quietly allow a re-call of the
Dispose method, and also generate an exception when accessing any other public methods or properties (since the object invariant has already been destroyed).
void Dispose( bool disposing)
{
if (disposed)
return ; //
//
disposed = true ;
}
public void SomeMethod()
{
if (disposed)
throw new ObjectDisposedException();
}
* This source code was highlighted with Source Code Highlighter .
7. (OPTIONAL) A class can inherit from
CriticalFinalizerObject , if the previous six points are few and you want more exotic. Inheritance from this class gives you additional guarantees:
- The finalizer of such classes is compiled by the JIT compiler immediately when constructing an instance, and not postponed as necessary. This makes it possible for the finalizer to succeed even in the case of an acute lack of memory.
- As we have said, the CLR does not guarantee the order of calling finalizers, which makes it impossible to access other objects containing unmanaged resources inside the finalizer. However, the CLR guarantees that the finalizers of “mere mortal” objects will be called before the heirs of the CriticalFinalizerObject . This makes it possible, in particular, from the finalizers of your classes (if they do not inherit from CriticalFinalizerObject ) to access the SafeHandle field, which will definitely be released later.
- Finalizers of such classes will be called even in case of an emergency unloading of the application domain.
// , ?
class Foo : CriticalFinalizerObject {}
* This source code was highlighted with Source Code Highlighter .
Pragmatic look at the Dispose pattern
If it seemed to you that working with resources in .NET is unjustifiably complex, then I have two news on this: one is good and the other is not very good. The “not so good” news is that working with resources is even more difficult than described here (*****), but the good thing is that in most cases all this complexity will hardly touch us.
All the complexities of implementing a Dispose pattern are associated with the assumption that the same class (or class hierarchy) can simultaneously contain both managed and unmanaged resources. But let's think about it, why should we even need to store unmanaged resources directly in business logic classes? But what about the notorious Single Responsibility Principle (SRP) and
Common Sense ? The RAII idiom, described earlier, has been used successfully for decades and is intended just for such cases:
if you have an unmanaged resource, then instead of working with it directly, wrap it in a managed shell and work with it .
If you look at the .NET Framework, you can see that it uses exactly this approach: a shell is created for all resources that hides all the complexity of working with resources inside, leaving the user to simply call the
Dispose method to explicitly clean up the resources (well, the finalizer, just in case). In addition, for the majority of unmanaged operating system resources, such shells have already been made, and there is no need to reinvent the wheel.
All this I lead to the fact that you
do not need to mix in your code business logic and logic for working with unmanaged resources . Both that, and another is rather difficult in itself and deserves a separate class. It turns out that this pattern is “optimized” for a very rare case (that a class can contain both managed and unmanaged resources), while making the most common case where a class contains only managed resources, very inconvenient in implementation and maintenance.
Simplified version of the Dispose pattern
If you and I know that no one is going to mix managed and unmanaged resources in one place, so why not implement this in code explicitly? We can leave the
Dispose method and instead of the additional Dispose method with a completely vague boolean parameter, add a virtual method
DisposeManagedResources , whose name will clearly state that we have to release the managed resources. The access modifier of this method should be similar to our
Dispose ( bool) method, i.e.
protected virtual for non-sealed classes or
private for sealed classes.
class SomethingWithManagedResources : IDisposable
{
public void Dispose()
{
// Dispose(true) GC.SuppressFinalize()
DisposeManagedResources();
}
// ,
protected virtual void DisposeManagedResources() {}
}
* This source code was highlighted with Source Code Highlighter .
At first glance, such an approach may seem too pragmatic, but judge for yourself: in the
Framework Design Guidelines book, two dozen pages are devoted to the Dispose of the pattern, while its authors recommend adding finalizers only when absolutely necessary. At the same time, we all know that mixing two types of resources in one class is bad, but still we follow a pattern that encourages it and does not prohibit it.
Conclusion
When developing library classes or business classes that will be used in a dozen other projects, it is quite reasonable to follow all the principles described above. Other requirements are imposed on reusable code, and when designing them, other principles must be followed: ease of use and extensibility of such classes are much more important than maintenance costs.
If you are designing business logic classes or simple libraries with a limited number of users, then you can not fool your head with “canons”, but use a simplified version of this pattern that works only with managed resources.
------------------------------
(*) In C ++, unlike C #, memory is also a resource. Therefore, the RAII idiom in C ++ is used both for freeing dynamically allocated memory and for freeing any other resources, such as OS descriptors or sockets.
(**) In Java 7, a construction similar to that
using the C # language has finally appeared: try-with-resource statement
(***) Unfortunately, the same syntax (tilde followed by the name of the class) is used for finalizers in C #, which is used for destructors in C ++. But the semantics of the destructor and the finalizer are very different, because the destructor implies the deterministic release of resources, but the finalizer is not.
(***) Yes, this is another difference in the behavior of .NET and the C ++ language. In the latter, the destructor is called only for a fully constructed object, and destructors are called for all of its fully constructed fields.
(****) Here, for example, I did not talk about how to get a “resource leak” when exceptions occur or about problems with changeable types that implement the
IDisposable interface. I already wrote about this earlier in the notes “
Guarantees of security of exceptions ” and “
On the harm of changeable significant types ”, respectively.