Destructors and Finalizers - Understanding Object Cleanup in C#
Vaibhav • September 10, 2025
In the previous article, we explored constructors - the special methods that initialize objects when they are created. Now we turn to the other end of an object’s lifecycle: what happens when an object is no longer needed? How does C# clean up resources and memory? This is where destructors and finalizers come into play.
In this article, we’ll explore how C# handles object cleanup, what destructors and finalizers are, when they are used, and how they relate to the garbage collector. We’ll also discuss best practices for managing unmanaged resources and how to implement cleanup logic safely and effectively.
Object Lifecycle and Memory Management
In C#, memory for objects is allocated on the heap when you use the new
keyword. The .NET runtime uses a garbage collector (GC) to automatically reclaim memory when
objects are no longer in use. This means you don’t need to manually delete objects as you would in languages
like C++.
However, some objects hold resources that are not managed by the garbage collector - such as file handles, database connections, or network sockets. These unmanaged resources need to be released explicitly. That’s where destructors and finalizers come in.
What is a Destructor?
A destructor is a special method that is called when an object is being reclaimed by the
garbage collector. In C#, destructors are written using a tilde (~
) followed by
the class name. They cannot be called directly and do not take parameters.
class FileLogger
{
private string filePath;
public FileLogger(string path)
{
filePath = path;
// Open file or allocate resource
}
~FileLogger()
{
// Cleanup logic
Console.WriteLine("Destructor called for FileLogger");
// Close file or release resource
}
}
In this example, the FileLogger
class defines a destructor that prints a
message when the object is finalized. This method is called by the garbage collector before the object is
removed from memory.
You cannot predict exactly when the destructor will run. The garbage collector decides when to collect objects based on memory pressure and other factors.
Finalizers and the GC
In .NET terminology, a destructor is actually a finalizer. The compiler translates the
destructor syntax into an override of the Object.Finalize()
method. This method
is called by the garbage collector before the object is removed.
Finalizers are placed in a special queue and run on a dedicated finalizer thread. This means they are not executed immediately when the object becomes unreachable. If your class has a finalizer, it may take longer to be collected.
Finalizers are useful for cleaning up unmanaged resources, but they come with performance costs. Use them only when necessary.
When to Use a Finalizer
You should use a finalizer only when your class directly holds unmanaged resources - such as native handles, unmanaged memory, or COM objects. If your class only uses managed resources (like strings, lists, or other .NET objects), you do not need a finalizer.
Most of the time, you should use the IDisposable
interface and the Dispose()
method to release resources deterministically. Finalizers are a
fallback mechanism in case Dispose()
is not called.
Implementing IDisposable
The recommended pattern for resource cleanup in C# is to implement the IDisposable
interface. This allows consumers of your class to call Dispose()
explicitly when they are done.
class FileLogger : IDisposable
{
private FileStream stream;
private bool disposed = false;
public FileLogger(string path)
{
stream = new FileStream(path, FileMode.Create);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Dispose managed resources
stream?.Dispose();
}
// Cleanup unmanaged resources (if any)
disposed = true;
}
}
~FileLogger()
{
Dispose(false);
}
}
This pattern ensures that resources are released properly. The Dispose()
method
can be called explicitly, and the finalizer acts as a backup in case it isn’t. The call to GC.SuppressFinalize(this)
tells the garbage collector that the finalizer is no
longer needed.
Using the Dispose Pattern
When you use a class that implements IDisposable
, you should call Dispose()
when you’re done. The easiest way to do this is with a using
statement.
using (FileLogger logger = new FileLogger("log.txt"))
{
// Use logger
} // Dispose() is called automatically here
The using
statement ensures that Dispose()
is
called even if an exception occurs. This is the preferred way to manage resources in C#.
Avoiding Finalizers When Possible
Finalizers add overhead to garbage collection. Objects with finalizers take longer to collect and require an
extra GC cycle. If you can release resources using Dispose()
, you should avoid
using a finalizer.
If your class does not hold unmanaged resources directly, do not implement a finalizer. Rely on the garbage
collector to clean up managed memory, and use Dispose()
for any cleanup logic.
Best Practices for Cleanup
Here are some guidelines for managing object cleanup in C#:
- Use
IDisposable
andDispose()
for deterministic cleanup. - Use
using
statements to ensureDispose()
is called. - Implement a finalizer only if your class holds unmanaged resources directly.
- Use the dispose pattern to support both
Dispose()
and finalization. - Call
GC.SuppressFinalize(this)
inDispose()
to avoid unnecessary finalization.
Prefer IDisposable
over finalizers.
Finalizers should be used only as a safety net for unmanaged resources.
Summary
Destructors and finalizers are mechanisms for cleaning up resources when an object is no longer needed. In C#, the garbage collector handles memory management automatically, but unmanaged resources require explicit cleanup. Finalizers provide a way to release these resources, but they are non-deterministic and should be used sparingly.
The recommended approach is to implement the IDisposable
interface and use the
dispose pattern. This allows for deterministic cleanup and avoids the performance costs of finalization. By
following these practices, you can ensure that your objects are cleaned up safely and efficiently.
In the next article, we’ll explore Access Modifiers - how to control visibility and access to class members, and how to design secure and maintainable APIs.