Skip to main content

C# Destructors

Introduction

In C# object-oriented programming, memory management is largely handled by the .NET Framework's Garbage Collector. However, there are situations when you need more control over resource cleanup. This is where destructors come into play.

A destructor is a special method that gets called when an object is being destroyed. Unlike constructors, which initialize objects, destructors clean up resources before an object is removed from memory. They are particularly useful when your class uses unmanaged resources like file handles, database connections, or network resources.

Understanding Destructors in C#

Basic Syntax

In C#, a destructor is defined using a tilde (~) followed by the class name:

csharp
class MyClass
{
// Constructor
public MyClass()
{
Console.WriteLine("Constructor called");
}

// Destructor
~MyClass()
{
Console.WriteLine("Destructor called");
}
}

How Destructors Work

Destructors in C# have several important characteristics:

  1. They cannot be called directly; the garbage collector invokes them
  2. They cannot take parameters
  3. They cannot be overloaded (a class can have only one destructor)
  4. They cannot have access modifiers
  5. They run on a separate thread (the finalization thread)
  6. The execution order is non-deterministic

When Destructors Are Called

Let's look at a simple example to understand when destructors are called:

csharp
using System;

class Program
{
static void Main()
{
Console.WriteLine("Program started");

// Create an object in a block
{
MyClass obj = new MyClass();
Console.WriteLine("Object created");
}

Console.WriteLine("Block exited");

// Force garbage collection to demonstrate destructor call
GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine("Program ending");
}
}

class MyClass
{
public MyClass()
{
Console.WriteLine("Constructor called");
}

~MyClass()
{
Console.WriteLine("Destructor called");
}
}

Output:

Program started
Constructor called
Object created
Block exited
Destructor called
Program ending

Notice that the destructor is called after the object goes out of scope and the garbage collection occurs.

Practical Use Case: Managing Unmanaged Resources

One of the main purposes of destructors is to clean up unmanaged resources. Here's an example managing a file handle:

csharp
using System;
using System.IO;
using System.Runtime.InteropServices;

class FileHandler
{
// Unmanaged resource - file handle
private IntPtr fileHandle;
private bool disposed = false;

public FileHandler(string filename)
{
// Simulate opening a file and getting a handle
Console.WriteLine($"Opening file: {filename}");
fileHandle = new IntPtr(123456); // Simulated handle value
}

// Method to use the file
public void ReadFile()
{
if (disposed)
throw new ObjectDisposedException("FileHandler");

Console.WriteLine("Reading from file...");
}

~FileHandler()
{
Cleanup();
}

private void Cleanup()
{
if (!disposed)
{
Console.WriteLine("Closing file handle in destructor");
// In real code we would call appropriate API to close handle
// CloseHandle(fileHandle);
fileHandle = IntPtr.Zero;
disposed = true;
}
}
}

class Program
{
static void Main()
{
UseFileHandler();

Console.WriteLine("After using file handler");
GC.Collect();
GC.WaitForPendingFinalizers();

Console.WriteLine("Program ending");
}

static void UseFileHandler()
{
FileHandler handler = new FileHandler("example.txt");
handler.ReadFile();
}
}

Output:

Opening file: example.txt
Reading from file...
After using file handler
Closing file handle in destructor
Program ending

Destructors vs. IDisposable Pattern

While destructors are useful, C# provides a better approach for resource cleanup through the IDisposable pattern. Here's a comparison:

DestructorIDisposable.Dispose
Called by the garbage collectorCalled explicitly by code
Non-deterministic timingImmediate, deterministic cleanup
Runs on the finalizer threadRuns on the same thread as the caller
Used as a safety netUsed as the primary cleanup mechanism

In most cases, it's better to implement the IDisposable pattern and use the destructor as a backup:

csharp
using System;

class ResourceManager : IDisposable
{
private bool disposed = false;

public ResourceManager()
{
Console.WriteLine("Resource allocated");
}

// Public implementation of IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Prevent destructor from running
}

// Protected implementation of Dispose pattern
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Dispose managed resources
Console.WriteLine("Managed resources cleaned up");
}

// Clean up unmanaged resources
Console.WriteLine("Unmanaged resources cleaned up");
disposed = true;
}
}

// Destructor as a backup
~ResourceManager()
{
Dispose(false);
}
}

class Program
{
static void Main()
{
// Using statement ensures Dispose is called
using (ResourceManager resource = new ResourceManager())
{
Console.WriteLine("Using the resource");
}

Console.WriteLine("After using block");

// Create another resource but don't dispose it explicitly
{
ResourceManager resource2 = new ResourceManager();
Console.WriteLine("Using second resource");
}

Console.WriteLine("Force garbage collection");
GC.Collect();
GC.WaitForPendingFinalizers();
}
}

Output:

Resource allocated
Using the resource
Managed resources cleaned up
Unmanaged resources cleaned up
After using block
Resource allocated
Using second resource
Force garbage collection
Unmanaged resources cleaned up

Notice how the second resource is only cleaned up after garbage collection, while the first is immediately cleaned up via the using statement.

Best Practices for Using Destructors

  1. Use sparingly: Only use destructors when absolutely necessary
  2. Prefer IDisposable: Implement the IDisposable pattern for deterministic cleanup
  3. Keep them simple: Destructors should be fast and not throw exceptions
  4. Don't reference other objects: Avoid calling methods on other objects in destructors
  5. Use as a safety net: Destructors should be your last line of defense for cleanup

Common Pitfalls

  1. Resource leaks: Relying solely on destructors can lead to resource leaks
  2. Performance impact: Objects with destructors are placed in a special finalization queue which adds overhead
  3. Non-deterministic timing: You can't predict when destructors will be called
  4. Exceptions in destructors: These can crash your application

Summary

Destructors in C# provide a way to clean up unmanaged resources when an object is garbage collected. However, they should be used sparingly and primarily as a backup mechanism. The preferred approach for resource cleanup is to implement the IDisposable pattern, which provides deterministic cleanup.

Key points to remember:

  • Destructors are called automatically by the garbage collector
  • They can't be called directly, can't take parameters, and can't be overloaded
  • They're primarily used for unmanaged resource cleanup
  • The IDisposable pattern is generally preferred over relying on destructors
  • Destructors should be simple, fast, and not throw exceptions

Exercises

  1. Create a class that simulates a database connection and uses a destructor to clean up the connection if it's not closed explicitly.

  2. Modify the example above to implement the full IDisposable pattern with both a destructor and a Dispose method.

  3. Write a program that creates multiple objects with destructors and observe the order in which the destructors are called.

  4. Research and explain why destructors in C# are actually implemented as the Finalize() method.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)