Introducing the Concepts
It is easy to get a bit confused when trying to figure out what resources you need to free explicitly and how to release the resources that your class has created or is holding references to. You will find that there is something called Finalizer, a clean-up pattern called The Dispose Pattern, and a special Deconstruct method that you can declare. In most cases though, you don't have to do anything and just let the Garbage Collector (GC) do its thing.However, if your class allocates unmanaged memory, open file handles, network sockets, database connections or similar, you need to ensure that these are properly closed and freed before the references are removed. I will go through the different concepts and hopefully clear out how to ensure that all resources are properly freed in your application.
The Deconstruct method
You might say that it is incorrect to even mention the Deconstruct keyword/method in this post, since it actually have nothing to do with freeing up resources or Garbage Collecting. However, that is exactly the reason why I choose to include it. If you are used to working with OOP in any other language you are most likely familiar with the concept of Constructors and Destructors. Whenever you create a new instance of a class it's Constructor is called and when the object is freed the Destructor is called before the object is removed, right?In C# you can define a special Deconstruct method with the following signature:
public void Deconstruct(out Type1 var1, out Type2 var2, ..., out TypeN, varN);
However, this method is not a Destructor, it is a DeCONstructor, and is not automatically invoked when the object is removed. It is just a helper method to deconstruct a user defined type into a Tuple. However, it's name makes it easy to confuse with a Destructor (now you can avoid doing this). If you want to know more about Deconstruct you can find an example on how to use it in the C# documentation. Otherwise, just remember that it has nothing to do with freeing resources and let's move on.
The Finalizer
In C# the Destructor is called Finalizer. If you define a Finalizer in your class it will be called as part of the Garbage Collection procedure. However, you do not have control over exactly when the Finalizer is called so do not assume it has been called just because any variables holding references to an object goes out of scope. In C# the Finalizer method must have the same name as the class, must not have any access level attributes (such as private or public) or return type, cannot take any input parameters, and must be prefixed with a tilde (~) character.~MyClass()
{
...
}
You cannot call the Finalizer from your application code since it is done automatically by the GC. Also, do not add a Finalizer to your class unless you really need one. Doing so will keep the object in memory for an additional amount of time which may have negative impact on your application's performance.
You can simply put all of your clean-up code in the Finalizer, but most of the time, if your class keeps resources that needs to be cleaned up, you want to give the user the opportunity to do so as soon as he is done with the object. This can be done by making your class Disposable.
The Dispose Pattern
Part of the .NET framework is the interface IDisposable. You can simply make your class Disposable by implementing the IDisposable interface which requires you to implement a method:public class MyClass : IDisposable
{
public void Dispose()
{
// Clean-up code goes here
}
}
which allows the application to wrap your class in a using directive:
using (var myObj = new MyClass())
{
// Do things with myObj
}
In the code above myObj is only accessible within the scope of the using block and as soon as it gets out of scope its Dispose method is automatically invoked.
Now, assume you put all the clean-up code in the Dispose method and remove the Finalizer. This works fine, as long as any user of MyClass either wraps it in a using directive or remember to call the Dispose method explicitly. There is however, a way to ensure that all resources are freed even in the case that a user of MyClass forgets to Dispose it. The solution is called The Dispose Pattern.
Microsofts official documentation on the Dispose Pattern can be a bit overwhelming. I will try to summarize it in a way so that it is easy to understand how to implement it. First let's look at an implementation of the full Dispose Pattern (don't worry if you don't understand all parts of it, I will explain it later):
public class MyClass : IDisposable
{
private readonly IntPtr buffer; // unmanaged memory buffer that needs to be freed explicitly
private readonly SafeHandle resource; // resource that also implements the Disposable Pattern
private bool disposed;
public MyClass()
{
buffer = ...; // allocate memory for the buffer
resource = ...; // allocate the resource
}
~MyClass()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize();
}
protected virtual void Dispose(bool disposing)
{
if (disposed) return;
ReleaseBuffer(buffer);
if (disposing)
{
resource?.Dispose();
}
disposed = true;
}
public void MyMethod()
{
if (disposed) throw new ObjectDisposedException(...);
...
}
}
Let's go through the code top to bottom and cover the interesting parts. First you will notice that the class implements the IDisposable interface, hence it must implement a public Dispose() method.
Next you'll find three different private fields, a boolean indicating if the object has been disposed or not, a pointer to unmanaged memory, and a reference to a class that is also Disposable. These have to be treated a bit differently, I'll explain why and how later.
After that comes the Constructor where the unmanaged memory buffer and the SafeHandle resources are allocated.
Then comes the Finalizer, which will be called automatically by the GC, but only if the Dispose method hasn't been called. This happens since the Dispose method implements a call to GC.SuppressFinalize() which tells the GC that it can collect the memory directly and skip the invocation of the Finalizer. Both the Finalizer and the Dispose method calls a custom Dispose method that takes a boolean as input. This boolean indicates if the method was called from the Dispose method or from the Finalizer.
All the clean-up code is put inside the Dispose(bool disposing) method. The purpose of the disposing flag is to determine whether you should call the Dispose method of any resources you have allocated that also are Disposable. You should not do that if the method is invoked from the Finalizer, and the reason for that is that Finalizers are called in an non-deterministic order, so the Finalizer of the underlying resource may already have been invoked. Non-disposable resources, like the unmanaged memory buffer, should however be freed in both cases.
Finally I have included a method MyMethod that checks if the object has been disposed and in that case throws an exception. This is to protect from bugs in the application that uses MyClass that might lead to unexpected behavior.
Kommentarer
Skicka en kommentar