C# Destructors and Finalizers
Introduction to Destructors in C#
In C#, destructors (also called finalizers) are special methods that are automatically called when an object is being garbage collected. They provide a mechanism to perform cleanup operations for unmanaged resources before an object is destroyed.
Destructors are non-deterministic and rely on the .NET garbage collector for execution.
Destructor Syntax and Characteristics
- Destructors have the same name as the class preceded by a tilde (~)
- They cannot have access modifiers, parameters, or return types
- They cannot be called explicitly; only the garbage collector can invoke them
- Each class can have only one destructor
- They are inherited and called in derivation order (from most derived to base)
Basic Destructor Example
This example shows a simple class with a destructor that prints a message when the object is garbage collected.
using System;
class SimpleClass
{
private string name;
public SimpleClass(string name)
{
this.name = name;
Console.WriteLine($"Constructor called for {name}");
}
// Destructor (Finalizer)
~SimpleClass()
{
Console.WriteLine($"Destructor called for {name}");
}
public static void Main()
{
SimpleClass obj = new SimpleClass("TestObject");
obj = null; // Make eligible for garbage collection
// Force garbage collection (for demonstration only)
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Main method completed");
}
}
Constructor called for TestObject Destructor called for TestObject Main method completed
IDisposable Pattern for Deterministic Cleanup
C# provides the IDisposable interface for explicit, deterministic resource cleanup using the Dispose pattern.
using System;
class ResourceHandler : IDisposable
{
private string resourceName;
private bool disposed = false;
public ResourceHandler(string name)
{
resourceName = name;
Console.WriteLine($"Resource acquired: {resourceName}");
}
public void UseResource()
{
if (disposed)
throw new ObjectDisposedException(nameof(ResourceHandler));
Console.WriteLine($"Using resource: {resourceName}");
}
// Public Dispose method (part of IDisposable)
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Prevent finalizer from running
}
// Protected virtual Dispose method for inheritance
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Dispose managed resources
Console.WriteLine($"Disposing managed resources: {resourceName}");
}
// Dispose unmanaged resources
Console.WriteLine($"Cleaning up unmanaged resources: {resourceName}");
disposed = true;
}
}
// Destructor (backup for when Dispose isn't called)
~ResourceHandler()
{
Dispose(false);
}
public static void Main()
{
// Using statement ensures Dispose is called automatically
using (var resource = new ResourceHandler("DatabaseConnection"))
{
resource.UseResource();
} // Dispose() called automatically here
Console.WriteLine("Resource cleanup completed");
}
}
Resource acquired: DatabaseConnection Using resource: DatabaseConnection Disposing managed resources: DatabaseConnection Cleaning up unmanaged resources: DatabaseConnection Resource cleanup completed
Using Statement for Automatic Cleanup
The using statement provides a convenient syntax for ensuring that Dispose() is called automatically, even if exceptions occur.
using System;
class FileProcessor : IDisposable
{
private string fileName;
public FileProcessor(string name)
{
fileName = name;
Console.WriteLine($"File opened: {fileName}");
}
public void ProcessFile()
{
Console.WriteLine($"Processing file: {fileName}");
}
public void Dispose()
{
Console.WriteLine($"File closed: {fileName}");
// Actual file closing logic would go here
}
public static void Main()
{
// Multiple resources can be used in one using statement
using (var file1 = new FileProcessor("data1.txt"),
file2 = new FileProcessor("data2.txt"))
{
file1.ProcessFile();
file2.ProcessFile();
} // Both Dispose() methods called here
// Alternative syntax for using statement
using (var file3 = new FileProcessor("data3.txt"))
{
file3.ProcessFile();
}
}
}
File opened: data1.txt File opened: data2.txt Processing file: data1.txt Processing file: data2.txt File closed: data2.txt File closed: data1.txt File opened: data3.txt Processing file: data3.txt File closed: data3.txt
Destructor Inheritance Chain
Destructors are called automatically in reverse order of inheritance (most derived first, then base classes).
using System;
class BaseClass
{
protected string className;
public BaseClass(string name)
{
className = name;
Console.WriteLine($"Base constructor: {className}");
}
~BaseClass()
{
Console.WriteLine($"Base destructor: {className}");
}
}
class DerivedClass : BaseClass
{
public DerivedClass(string name) : base(name)
{
Console.WriteLine($"Derived constructor: {className}");
}
~DerivedClass()
{
Console.WriteLine($"Derived destructor: {className}");
}
}
class Program
{
static void Main()
{
DerivedClass obj = new DerivedClass("TestObject");
obj = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
Base constructor: TestObject Derived constructor: TestObject Derived destructor: TestObject Base destructor: TestObject
Best Practices for Resource Management
- Prefer IDisposable and using statements over destructors for deterministic cleanup
- Implement the full Dispose pattern when managing unmanaged resources
- Only use destructors as a safety net when Dispose might not be called
- Call GC.SuppressFinalize(this) in Dispose() to avoid redundant cleanup
- Use using statements for short-lived resource ownership
- Avoid performing time-consuming operations in destructors
- Don't reference other managed objects in destructors (they may already be collected)
- Consider using SafeHandle for wrapping unmanaged resources
Comparison Table
Feature | Destructor (~Class) | IDisposable Pattern |
---|---|---|
Deterministic | No | Yes |
Control | Garbage Collector | Developer |
Performance | Poor | Good |
Reliability | Low | High |
Inheritance | Automatic chaining | Manual implementation |
Recommended Use | Backup cleanup | Primary resource management |