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:

  1. 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.
  2. The static steps also run Derived first, then Base. new Derived triggers Derived's static constructor; Base's static constructor runs only when the Derived constructor calls base(...).
  3. When Derived() forwards to this("default"), the field initializers run once. The compiler puts them only in constructors that call base(...), not in those that forward with this(...).
Flow of nine steps. new Derived("Kan"), then Derived static field initializer, Derived static constructor, Derived instance field initializer, Base static field initializer, Base static constructor, Base instance field initializer, the Base constructor body and finally the Derived constructor body. The four static steps are marked first time only.
What actually ran for the first new Derived("Kan") on .NET 10. Steps marked "first time only" run once per type; for the second object only the other four steps ran.

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() plus WaitForPendingFinalizers() 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' Dispose from your own Dispose.
  • If a class wraps an OS handle or other unmanaged resource directly, Microsoft Learn recommends wrapping it in a SafeHandle rather than writing your own finalizer.
  • For how using relates to try/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
Bar chart. Plain class: 5.1 ms. Class with a finalizer whose Dispose calls GC.SuppressFinalize: 35.9 ms. Class with a finalizer that is never disposed: 181.3 ms, about 35 times the plain class.
Allocating 1,000,000 small objects and then forcing a full GC plus WaitForPendingFinalizers. Average of 3 measured runs after a warm-up. .NET 10.0.14, Release build, Apple Silicon Mac. Shorter is faster.

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.

  • #C#
  • #constructors
  • #finalizers
  • #IDisposable
  • #garbage collection

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

About · Apps

↑