You need no Finalizer or Why your IDisposable implementation is wrong

iamprovidence
13 min readApr 7, 2024

--

If you think writing memory-efficient applications is hard, then you should ask a C++ guy. He will tell you what “hard” actually means. Oh dear, believe me, he will😂

Most modern languages have built-in support for garbage collection, and C# is not an exception here. Despite that, developers continue making the same mistakes over and over again. Don’t want to be one of those? Then stay with me.

Today we’ll review the memory management process in C#. You will see the difference between Finalizer and Dispose(). Why your IDisposable implementation is wrong. We will discuss why there is no need for a Finalizer. And most importantly, you will learn how to implement IDisposable properly.

If you are ready, let’s get right into it.

A recap

Before start talking about Dispose() we need a small recap on how memory is managed.

In C# we can create variables either of value or reference type. As names imply, value types store values themselves, while reference types only store a reference to an actual value stored somewhere else.

Most programming languages have introduced the concepts of stack and heap, which help manage memory efficiently. Simple value types, like int, bool, and char, are stored in the stack, while complex, reference types, like objects, are stored in the heap. It is done this way since stack allocation is faster, while heap allows storing bigger objects.

Take a look at the code below to see how the memory is allocated:

A variable’s lifetime is limited to its scope. As you can see, variables in the stack override previous values, while those in the heap remain. Therefore, stack does not require any clearing, while heap does. When there isn’t enough space in the heap, the garbage collection (GC) process is triggered.

During this process, the application stops working. All the objects that do not have a reference pointing at them will be removed. Then squashed together, to avoid data fragmentation (a state when there is enough total available space in the storage, but a big object can not fit in any of the available slots).

Surviving objects will be moved to the second generation. There are only three generations in .Net. Objects in a later generation will be examined only if garbage collection for the previous generation have not released enough space. When the process is done, the application can continue working.

So far, so good. Now, let’s see how IDisposable and Finalizer complete the picture.

Most of the time, our application handles in-built types. However, sometimes, you may need external resources of the Operating System (OS), such as file handlers, network sockets, and so on. They are called unmanaged since .Net does not know how to clean up those and they are completely under the developer’s responsibility.

Imagine, we capture those resources, and then never release them. .Net won’t do it for us either. Keep them for too long, and you will end up in resource starvation. Which will sooner or later lead to an application crash or even a crash of the Operating System.

To avoid this issue, it is important to release unmanaged resources as soon as possible by calling a Dispose() method. This way, your object is still in a heap, but it is no longer holding OS data.

However, it can happen so that our poor developer forgets to call Dispose(). We all are humans after all 🎵. We can not allow capturing those resources for the entire application lifespan. That is exactly when a Finalizer enters the stage.

Finalize is just like a Dispose(), clear unmanaged resources. The difference here is that Dispose() is called explicitly by a developer in the code as soon as a class is no longer needed, while Finalizer is called implicitly during garbage collection. Sure, it is not ideal to hold the unmanaged resource until GC is triggered, but at least it is something.

Notice, that if Finalizer is defined on the object, it will not be removed right away. After the garbage collection process is done, and application work is resumed, such an object will be moved to the Finalization Queue, where the Finalizer will be called.

And that, kids, is the simplified version of how memory is managed in C# 😅. There is much more to it, but those are enough for our purpose. If you are interested in learning more, let me know in the comments, meanwhile we will continue.

Key points

From the section above, you should have highlighted the next key points:

We need the Dispose() method to release unmanaged resources as soon as possible. It is called explicitly in the code by developers.

On the other hand, Finalizer is just insurance, to clear unmanaged resources during garbage collection, in case the developer forgets to call Dispose(). It is called implicitly, and we have no control over that.

An object, that has a Finalizer defined, will remain alive longer than necessary, by going through two “garbage collection” cycles. It can result in increased memory usage and decreased performance of the app.

IDisposable pattern

Alright. So we have learned that Dispose() is our homie. Finalizer is ultimately a bad thing. We need them both. Great! Let’s start coding.

At first glance, Dispose() and Finalizer seem identical, perform similar tasks, and behave almost in the same way. Implementing them, however, often leads to a lot of code duplication. Smart guys from Microsoft have looked at all the mess they’ve created and to avoid code duplication they came up with the IDisposable pattern:

class MyClass : IDisposable
{
// To detect redundant calls
private bool _isAlreadyDisposed;

// Finalizer
~MyClass()
{
CleanUp(disposing: false);
}

// IDisposable
public void Dispose()
{
CleanUp(disposing: true);
GC.SuppressFinalize(this);
}

protected virtual void CleanUp(bool disposing)
{
if (!_isAlreadyDisposed)
{
if (disposing)
{
// TODO: dispose managed objects
}

// TODO: free unmanaged resources
_isAlreadyDisposed = true;
}
}
}

In short, we still ended up implementing both Finalizer and Dispose(), while all the shared code was just moved to the CleanUp() method. 😕. Not the smartest solution if you ask me.

I know, I know. It is not the cure you were looking for. But don’t get disappointed too quickly. Such implementation actually makes sense.

Let’s see how it works.

Imagine you are a responsible developer, that always calls a Dispose(). Moreover, you have even made the code reliable by wrapping it up in try/finally:

var zhuLi = new MyClass();

try
{
zhuLi.DoTheThing();
}
finally
{
zhuLi?.Dispose(); // guaranteed Dispose call
}

Clumsy, isn’t it? That’s why Microsoft had to introduce some sugar in the form of using statement which is just hidden try/finally:

using var zhuLi = new MyClass(); // guaranteed Dispose call

zhuLi.DoTheThing();

So, let’s see what happens with our class when Dispose() is called:

  1. _isAlreadyDisposed will be false, so the CleanUp() method will be executed
  2. disposing is true in this case, so the managed object will be disposed
  3. then unmanaged resources are cleaned
  4. at the very end, _isAlreadyDispoed will be changed to true. This way calling Dispose() multiple times won’t clean up already cleaned resources
  5. but wait a second. After CleanUp() we are also calling GC.SuppressFinalize(). As the name implies, it will make, it so Finalizer will not be called. This means the object will not get to the finalization queue and will be completely removed in the first “garbage collection” cycle
class MyClass : IDisposable
{
// To detect redundant calls
private bool _isAlreadyDisposed;

. . .

// IDisposable
public void Dispose()
{
CleanUp(disposing: true);
// 5
GC.SuppressFinalize(this);
}

protected virtual void CleanUp(bool disposing)
{
// 1
if (!_isAlreadyDisposed)
{
// 2
if (disposing)
{
// TODO: dispose managed objects
}

// 3
// TODO: free unmanaged resources

// 4
_isAlreadyDisposed = true;
}
}
}

Alright, alright, this part makes sense 😌

Now, let’s see what will happen when you forget to call Dispose().

{
/*using*/ var zhuLi = new MyClass();

zhuLi.DoTheThing();

// zhuLi.Dispose();
}

The object won’t be cleared and unmanaged resources will be captured until the garbage collection process is triggered. Nobody knows when it will happen, but when it does, the next actions will be performed:

  1. object will appear in the finalization queue, from where the Finalizer will be called
  2. _isAlreadyDisposed will be false, so the method will be executed
  3. disposing is false, so managed objects will be untouched
  4. then unmanaged resources are cleaned
  5. at the very end, _isAlreadyDispoed will be changed to true, so we don’t clean it again
  6. and we have nothing to suppress since we are already in the Finalizer
class MyClass : IDisposable
{
// To detect redundant calls
private bool _isAlreadyDisposed;

. . .

// 1
// Finalizer
~MyClass()
{
CleanUp(disposing: false);
// 6
}

protected virtual void CleanUp(bool disposing)
{
// 2
if (!_isAlreadyDisposed)
{
// 3
if (disposing)
{
// TODO: dispose managed objects
}

// 4
// TODO: free unmanaged resources

// 5
_isAlreadyDisposed = true;
}
}
}

There are three questions to answer:

The third step is not obvious. Why wouldn’t we clean managed objects? 🤔

We would like to clear everything. However, we can not guarantee those objects are not in use anymore. If they are, it will lead to unexpected behavior in our app. On the other hand, if they are not used, GC knows about them, and they will be cleared the next time GC is triggered.

Does that mean unused managed objects will require 2 cycles of GC process to be cleared?

Yes, it does. This is why you should be responsible and call Dispose()🙃.

Wouldn’t the fourth step lead to unexpected behavior because of the same reasons? 🤔

Clearing unmanaged resources also can lead to unpredicted behavior, but we can not have the luxury of holding them for a long time. Other applications may need them too. If OS sees that our app holding them for too long, it may just terminate the app, which is even worse.

As we said earlier, Finalizer is a bad thing, which is why calling Dispose() is preferable. However, if you forget, this pattern would rather crash your app than crash the entire system.

Another thing, worth noticing CleanUp() is protected and virtual. This is done in case you want to inherit from this class and override cleaning behavior in descendant classes.

It is not ideal, but it works... Kind of 😒.

Why IDisposable is wrong

Alright. Screw all those educational examples. Behold the real code:

class OrderService : IDisposable
{
private bool _isAlreadyDisposed;
private AppDbContext _dbContext;

public OrderService(AppDbContext dbContext)
{
_isAlreadyDisposed = false;
_dbContext = dbContext;
}

~OrderService()
{
CleanUp(disposing: false);
}

public void Dispose()
{
CleanUp(disposing: true);
GC.SuppressFinalize(this);
}

protected virtual void CleanUp(bool disposing)
{
if (!_isAlreadyDisposed)
{
if (disposing)
{
_dbContext.Dispose();
}

_isAlreadyDisposed = true;
}
}
}

Isn’t it perfect 🤩? This is exactly what most IDisposable implementation looks like. We have Dispose() we have Finilizer. Everything, as the book says 😌. Just compare it to what you have in your project or any other open-source one. I am pretty much sure, they will look exactly the same. If that is the case, I have bad news for you 😬

What if I tell you, that such implementation is complete nonsense 😨. Don’t you see what is wrong here?

Not a surprise. That is what Microsoft has been feeding us. Additionally, I have fooled you in this trap, using all my puppet master skills 😏. Our thinking was reasonable, I have guided you through all the steps, but somehow we still went in the wrong direction.

Before continue reading, you can take some time and look at the code above. Maybe you can spot it yourself.

For those, who are as lazy as I am, let’s just continue. We will start with the CleanUp() method of OrderService.Some developers try to optimize if statements and rewrite them, so we might do that as well:

protected virtual void CleanUp(bool disposing)
{
if (_isAlreadyDisposed) return;

if (disposing)
{
_dbContext.Dispose();
}

_isAlreadyDisposed = true;
}

Now let’s just inline CleanUp() in both Finalizer and Dispose():

class OrderService : IDisposable
{
. . .

_isAlreadyDisposed = false;

~OrderService()
{
if (_isAlreadyDisposed) return;

if (false)
{
_dbContext.Dispose();
}

_isAlreadyDisposed = true;
}

public void Dispose()
{
if (_isAlreadyDisposed) return;

if (true)
{
_dbContext.Dispose();
}

_isAlreadyDisposed = true;
GC.SuppressFinalize(this);
}
}

Check out Finalizer. It does nothing! For the first call, it will change the value of _isAlreadyDisposed to true, and for the second one, it will literally do nothing!

Wait? What? So I don't need it? Why then Microsoft show it to us?🤔

That is the thing! You don’t need a Finalizer!

You should not blame Microsoft. They stated multiple times that in most cases, you don't need the full implementation of IDisposable. However, somehow millions of developers omit that and still continue implementing it in this completely wrong way. This is a common mistake not only among junior developers. I have seen seniors do it too!

Das ist nicht gut 👮‍♂️. So, let’s make IDisposable great again.

So Finalizer is completely redundant. Let’s just remove it. We will end up only with Dispose():

class OrderService : IDisposable
{
. . .

public void Dispose()
{
if (_isAlreadyDisposed) return;

if (true)
{
_dbContext.Dispose();
}

_isAlreadyDisposed = true;
GC.SuppressFinalize(this);
}
}

The Dispose() here is kind of wacky too. GC.SuppressFinalize(this) does nothing since we have no Finalizer. In the middle, we have an if statement that will always be executed. We can remove it as well. _isAlreadyDisposed prevents redundant calls and saves up performance. However, it is likely that all standard implementation of Dispose() already have it, so we can just call them right away.

As a result, we will end up with such code:

class OrderService : IDisposable
{
private AppDbContext _dbContext;

public OrderService(AppDbContext dbContext)
{
_dbContext = dbContext;
}

public void Dispose()
{
_dbContext.Dispose();
}
}

Waaait a second. What about that chunky IDisposable pattern? 😱

You don’t need it!

The same mistake is made in hundreds of projects. Every time, it is the same empty Finalizer, which does nothing, Dispose() with weird if statements. Numerous developers around the world implement the Dispose pattern wrongly not even questioning it.

We can go even further. AppDbContext does not belong to OrderService and is injected outside. Therefore, we have no privilege to dispose of it. That is the responsibility of the calling code.

var appDbContext = new AppDbContext();

{
var service = new OrderService(appDbContext);

// causes unpredicted behavior for underlying code
// service.Dispose();
}
{
var service = new OrderService(appDbContext);

service.MyMethod(); // throws ObjectDisposedException
}

// caller code should dispose AppDbContext
appDbcontext.Dispose();

Here is the right implementation of the class:

class OrderService
{
private AppDbContext _dbContext;

public OrderService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
}

There is no Dispose() at all!

If you are familiar with ASP, you may not even realize that it is exactly how most of our classes are implemented. You don’t see IDispose as often as you should. It is because of the DI container. It will create AppDbContext, inject it, and dispose of it when it is no longer needed. So we don’t have to worry about it at all.

I gueesss, I don’t need the Dispose() method. 😬
But when do I actually implement it?🤔

The only valid case for Dispose() is when you create disposable objects inside the constructor. This way the calling code, can see your class doing some shenanigans, and should be disposed.

class OrderService : IDisposable
{
private IDbContext _dbContext;

public OrderService()
{
_dbContext = new AppDbContext();
}

public void Dispose()
{
_dbContext.Dispose();
}
}

However, even such an approach is questionable. You are tied up to an implementation, not an abstraction, meaning it can not be easily changed, unit tested, and so on and so forth.

What about Finalizer? Do we even need it? 🤔

The short answer: No 😆

In most cases, it is not needed. For Finalizer to happen you need unmanaged resources which are hella rare. They are unmanaged because they run outside the .NET, for example, a call to a DLL in the Win32 API.

You know what? So far, we were just been talking about unmanned resources. It is time to see them:

public class FileWrapper : IDisposable
{
private IntPtr _handle;

public FileWrapper(string fileName)
{
_handle = CreateFile(fileName, 0, 0, 0, 0, 0, IntPtr.Zero);
}

public void Dispose()
{
CloseHandle(_handle);
}

[DllImport("kernel32.dll", EntryPoint="CreateFile", SetLastError=true)]
private static extern IntPtr CreateFile(
String lpFileName,
UInt32 dwDesiredAccess,
UInt32 dwShareMode,
IntPtr lpSecurityAttributes,
UInt32 dwCreationDisposition,
UInt32 dwFlagsAndAttributes,
IntPtr hTemplateFile); // no body 😱

[DllImport("kernel32.dll", SetLastError=true)]
private static extern bool CloseHandle(IntPtr hObject);
}

You did not expect that, weren’t you? 😏 Don’t worry. The chances you will be working with unmanaged resources are the same and you will face unsafe code. After all, unmanaged resources are just unsafe code. Now ask yourself when was the last time I’ve run across unsafe code? That is exactly the case. Never! 😆

I hope you have something to think through😅 Meanwhile, I will start closing down 🙃

Let’s call it a day

This article started as “You need no Finalizer” and ended as “You need no Dispose()” 😅 I didn’t want to reach that far, but farewell, if only one could see the future 🤷‍♂️.

I hope now you will have a better understanding of the Dispose pattern and how memory is managed in C#.

Even though managed resources seem to be vulnerable to memory leaks, in reality, as soon as you have Finalizer, they are not. Garbage collectors know about them, and will clean them. However, it is preferable to call Dispose() as soon as possible.

If you still struggle with distinguishing managed resources from unmanaged follow such rule:

Does it have Dispose()? Managed it is.

If it looks like somebody vomited in your code, it is unmanaged.

When it comes to implementing IDisposable it can be done in a variety of ways. Here is a guideline, to help you:

  1. If you inject objects through a constructor, you don’t need to implement IDisposable at all!
  2. If you create disposable objects inside the construct, you need the Dispose() method, and only it.
  3. If you create unmanaged resources in your class (99% not your case) you need both Dispose() and Finalizer to work in conjunction, and that is exactly where IDisposable pattern shies in all its glory.

I hope it was somehow useful for you🤓. If so don’t forget to give it a clap 👏 If you feel so, you can also honor my efforts with a cup of coffee using the link below☕️ Follow me, to get more articles about C# ✅ See you😉

--

--

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence