C# Constructors and Finalizers: Order, GC, Dispose
C# constructor chaining and static constructors in real run order, when finalizers run on .NET 10, why IDisposable wins, and what a finalizer costs.
Revised November 2026. This tutorial first appeared in Korean in 2021. We reran every example on .NET 10.0.14 (C# 14). The biggest difference is the destructor example: the 2021 article showed "c destroyed, b destroyed, a destroyed", but on .NET 10 not a single destroy message appears, because .NET Core and later do not run finalizers when the process exits. The claim that "the compiler always generates a default constructor" is also only true when you declare no constructor at all. This version fixes both, and adds the real run order of chained and static constructors, a demonstration of when finalizers actually run, and a measurement of what a finalizer costs.
This article is for developers learning C# constructors, and for anyone who put cleanup code in a destructor and found that it never ran. The short version: constructors run in a fixed order you can print and verify; finalizers run whenever the runtime decides, or not at all. Files, sockets and database connections should be released with IDisposable and using. A finalizer is, at most, a safety net for when someone forgets to call Dispose.
A note on names: the C# specification and Microsoft Learn now call ~ClassName() a finalizer. The older term "destructor" suggests C++ behavior, where the object is cleaned up the moment it leaves scope, and C# does not work that way. This article uses "finalizer" from here on.
Constructor basics: the 2021 examples on .NET 10
A constructor has the same name as its class, no return type, and runs when new creates an object. The original Car example still works as written (we shortened the output strings):
class Car
{
private int maxSpeed;
private int speed = 0;
private string model;
public Car(int maxSpeed, string model) { this.maxSpeed = maxSpeed; this.model = model; }
public void ShowCarInformation() => Console.WriteLine($"{model}: speed {speed}km/h, max {maxSpeed}km/h");
public void SpeedUp(int increment)
{
if (speed + increment > maxSpeed) Console.WriteLine($"Cannot exceed {maxSpeed}km/h.");
else { speed += increment; Console.WriteLine($"{model}: now {speed}km/h"); }
}
}
// new Car(325, "Gallardo"), then SpeedUp(50), (40), (210), (30)
Gallardo: speed 0km/h, max 325km/h
Gallardo: now 50km/h
Gallardo: now 90km/h
Gallardo: now 300km/h
Cannot exceed 325km/h.
One statement in the original needs correcting. "Even if you don't write a constructor, the compiler creates a default one" applies only to a class that declares no constructor at all. As soon as you add a constructor with parameters, as Car does, the parameterless one disappears:
Program.cs(1,15): error CS7036: There is no argument given that corresponds to the required parameter 'maxSpeed' of 'Car.Car(int, string)'
The constructor overloading example ran unchanged too. Adding two arguments the original did not try shows how overload resolution picks a constructor:
class MyClass
{
public MyClass() => Console.WriteLine("MyClass()");
public MyClass(int a) => Console.WriteLine("MyClass(int)");
public MyClass(double d) => Console.WriteLine("MyClass(double)");
}
_ = new MyClass(); _ = new MyClass(10); _ = new MyClass(25.5);
_ = new MyClass(10L); _ = new MyClass('A');
MyClass()
MyClass(int)
MyClass(double)
MyClass(double)
MyClass(int)
A long (10L) has no implicit conversion to int, so the double overload wins. A char ('A') converts to int, so the int overload wins. These are the same rules as for method overloading, covered in C# methods. C# 12 primary constructors, where the parameters follow the class name, are covered in C# classes.
Constructor chaining and the real run order
: this(...) calls another constructor of the same class first, and : base(...) calls the base class constructor first. To see the full order, we logged every step:
class Base
{
static int sField = Log.Say(" Base static field initializer");
static Base() => Log.Say(" Base static ctor");
int field = Log.Say(" Base instance field initializer");
public Base(string tag) => Log.Say($" Base({tag}) body");
}
class Derived : Base
{
static int sField = Log.Say(" Derived static field initializer");
static Derived() => Log.Say(" Derived static ctor");
int field = Log.Say(" Derived instance field initializer");
public Derived() : this("default") => Log.Say(" Derived() body");
public Derived(string name) : base(name) => Log.Say($" Derived({name}) body");
}
// new Derived("Kan"); then new Derived();
Main start
Derived static field initializer
Derived static ctor
Derived instance field initializer
Base static field initializer
Base static ctor
Base instance field initializer
Base(Kan) body
Derived(Kan) body
--- second object
Derived instance field initializer
Base instance field initializer
Base(default) body
Derived(default) body
Derived() body
Three things here surprise most people:
- The derived class's field initializers run before the base constructor. The C# compiler emits field initializers at the very top of the constructor, ahead of the
base(...)call. - The static steps also run Derived first, then Base.
new Derivedtriggers Derived's static constructor; Base's static constructor runs only when the Derived constructor callsbase(...). - When
Derived()forwards tothis("default"), the field initializers run once. The compiler puts them only in constructors that callbase(...), not in those that forward withthis(...).
Static constructors: once per type
A static constructor is written static ClassName(), with no access modifier and no parameters. The runtime calls it once per type, just before first use, and your code cannot call it directly.
static class Config
{
public static readonly string Value = Load();
static Config() => Console.WriteLine(" Config static ctor");
static string Load() { Console.WriteLine(" Config.Load()"); return "ok"; }
}
// read Config.Value twice
Config.Load()
Config static ctor
Config.Value read: ok
Config.Value again: ok
The static field initializer (Load()) runs before the static constructor body, and nothing runs again on the second access. That makes static constructors handy for loading settings or preparing a cache, with one caveat that we also tested: if a static constructor throws, the type is broken for the rest of the process. We made one throw and accessed the type twice. The static constructor ran only once, and both accesses threw TypeInitializationException (wrapping InvalidOperationException). There is no retry, so keep network or file I/O that can fail out of static constructors.
When does a finalizer actually run?
We ran the original destructor example on .NET 10 with only its strings translated, three times as a Debug build and three times as Release:
class MyClass
{
private string name;
public MyClass(string name) { this.name = name; Console.WriteLine(name + " created!"); }
~MyClass() { Console.WriteLine(name + " destroyed!"); }
}
// Main: new MyClass("a"); new MyClass("b"); new MyClass("c");
a created!
b created!
c created!
All six runs were identical. "destroyed!" never appeared. .NET Framework ran pending finalizers on process exit; .NET Core and later do not. The 2021 output, with the objects destroyed in the order c, b, a, appears to come from the .NET Framework of the time and cannot be reproduced on today's runtime.
To watch finalizers run, you have to force a collection and then wait for the finalizer thread. The objects must be created in a separate method:
[MethodImpl(MethodImplOptions.NoInlining)]
static void Make(string name) => _ = new MyClass(name);
Make("a"); Make("b"); Make("c");
Console.WriteLine("before GC.Collect");
GC.Collect();
Console.WriteLine("after GC.Collect");
GC.WaitForPendingFinalizers();
Console.WriteLine("after WaitForPendingFinalizers");
a created (thread 1)
b created (thread 1)
c created (thread 1)
before GC.Collect
after GC.Collect
a finalized (thread 2)
c finalized (thread 2)
b finalized (thread 2)
after WaitForPendingFinalizers
What this shows:
- Finalizers run on the finalizer thread (2), not the main thread (1). That is why a finalizer must never touch UI objects or anything tied to a particular thread.
- The order was a, c, b, the same in all five runs. It is neither creation order nor reverse order: finalization order is not guaranteed.
- When we created the same objects as local variables directly in
Main,GC.Collect()plusWaitForPendingFinalizers()finalized nothing, in Debug and in Release. In these runs the locals were treated as alive until the method returned. If your finalizer test "doesn't work", this is usually why.
Finalizers cannot be inherited, overloaded or called. Calling one gives error CS0122: 'Res.~Res()' is inaccessible due to its protection level, and declaring one on a struct gives error CS0575: Only class types can contain destructors.
Use IDisposable and using for cleanup
Instead of a finalizer whose timing nobody controls, IDisposable lets your code decide when cleanup happens. A using block, or a C# 8 using declaration, calls Dispose() the moment the scope ends, even if an exception is thrown.
sealed class Resource : IDisposable
{
private readonly string name;
private bool disposed;
public Resource(string name) { this.name = name; Console.WriteLine($" open {name}"); }
public void Dispose()
{
if (disposed) return;
disposed = true;
Console.WriteLine($" Dispose {name}");
GC.SuppressFinalize(this); // already cleaned up: skip the finalizer
}
~Resource() => Console.WriteLine($" finalizer {name}: Dispose was not called!");
}
using (var r = new Resource("file1")) { Console.WriteLine(" working with file1"); }
static void UseDeclaration()
{
using var r = new Resource("file2"); // C# 8 using declaration
Console.WriteLine(" working with file2");
} // Dispose runs here
open file1
working with file1
Dispose file1
open file2
working with file2
Dispose file2
--- forgot Dispose
open file3
finalizer file3: Dispose was not called!
file3 is the case where someone forgot Dispose. Here the finalizer acted as a safety net only because we forced a GC. In a real program you do not know when it will run, and if the process exits first it will not run at all. The rules we follow:
- If a class holds only managed resources (a
FileStream, an HTTP response, a database connection), do not write a finalizer. Call those objects'Disposefrom your ownDispose. - If a class wraps an OS handle or other unmanaged resource directly, Microsoft Learn recommends wrapping it in a
SafeHandlerather than writing your own finalizer. - For how
usingrelates totry/finally, see C# exception handling.
Measured: what a finalizer costs
The 2021 article said only that unnecessary destructors "can hurt performance". We measured by how much. We created one million instances of a class with a single int field, discarding each immediately, then ran GC.Collect(), WaitForPendingFinalizers() and GC.Collect() again. Release build, .NET 10.0.14, Apple Silicon Mac, one warm-up round and three measured rounds.
| Class | Allocation loop (3 runs) | GC + finalization (3 runs) | Bytes/object | Finalizers run |
|---|---|---|---|---|
| Plain class | 3.9 / 3.9 / 3.9 ms | 1.2 / 1.2 / 1.2 ms | 24 B | 0 |
| Finalizer + Dispose (SuppressFinalize) | 32.6 / 32.4 / 31.9 ms | 3.6 / 3.5 / 3.6 ms | 24 B | 0 |
| Finalizer, never disposed | 169.8 / 173.4 / 156.4 ms | 14.7 / 14.7 / 14.9 ms | 24 B | 1,000,000 |
All three objects are the same 24 bytes, yet the total time went from 5.1 ms to 181.3 ms, about 35 times longer. Every object with a finalizer is registered with the runtime's finalization queue when it is created. When the GC finds it unreachable it cannot free it immediately: the object survives, waits for the finalizer thread, and its memory is reclaimed only in a later collection. GC.SuppressFinalize removes the object from that queue and brings the total down to 35.9 ms, but the registration cost remains, about seven times the plain class. The fastest finalizer is the one you never write.
Summary
| You want to | Use |
|---|---|
| Require values when creating an object | A constructor (or required members) |
| Share setup code between constructors | : this(...) chaining |
| Prepare something once per type | Static constructor or static field initializer |
| Release files, connections and the like promptly | IDisposable + using |
| Own an unmanaged handle | SafeHandle (not a hand-written finalizer) |
| A safety net if Dispose is forgotten | Finalizer + GC.SuppressFinalize in Dispose |
Tested with .NET SDK 10.0.403 / runtime 10.0.14, C# 14 (the net10.0 default), macOS on an Apple Silicon Mac.
FAQ
Do C# finalizers (destructors) run when the program exits?
Not on .NET Core and later, including .NET 5 through .NET 10. Running the 2021 example on .NET 10 printed the three 'created' lines and none of the 'destroyed' lines. Anything that must be cleaned up has to be released explicitly with IDisposable and using.
When and how often does a static constructor run?
Once per type, just before the first static member access or the first instance is created. Static field initializers run before the static constructor body, and when you first create a derived class, its static constructor runs before the base class's. We confirmed both with real output.
How much does a finalizer slow things down?
Creating one million small objects and then finishing a full GC took 5.1 ms for a plain class and 181.3 ms for a class with a finalizer, about 35 times longer (.NET 10, Release build, Apple Silicon Mac). Calling GC.SuppressFinalize in Dispose brought it down to 35.9 ms.