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 and Dispose() for deterministic cleanup.
  • Use using statements to ensure Dispose() 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) in Dispose() 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.