Das Singleton-Muster stellt sicher, dass eine Klasse während der gesamten Lebensdauer einer Anwendung genau eine Instanz hat und bietet einen globalen Zugriffspunkt auf diese Instanz. Dieser Leitfaden behandelt mehrere Implementierungsansätze in C# .NET, vom einfachsten bis zum robustesten, zusammen mit Hinweisen, wann Singletons angemessen sind und wann Sie nach Alternativen greifen sollten.

Wann ein Singleton verwendet werden sollte

Singletons sind nützlich, wenn:

  • Sie genau eine Instanz einer Klasse benötigen, um Aktionen im gesamten System zu koordinieren (zum Beispiel ein Konfigurationsmanager, Logging-Dienst oder Verbindungspool).
  • Das Erstellen mehrerer Instanzen Konflikte verursachen oder Ressourcen verschwenden würde.
  • Die Instanz global zugänglich sein muss, ohne sie durch jeden Methodenaufruf durchzureichen.

Implementierung 1: Grundlegend (nicht thread-sicher)

Dies ist die einfachste Implementierung, aber sie ist nicht sicher für Multithread-Anwendungen:

public sealed class Singleton
{
    private static Singleton _instance;

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new Singleton();
            }
            return _instance;
        }
    }
}

Problem: Wenn zwei Threads gleichzeitig _instance == null prüfen, werden beide eine Instanz erstellen, was die Singleton-Garantie verletzt.

Implementierung 2: Thread-sicher mit Lock

Das Hinzufügen einer lock-Anweisung verhindert, dass mehrere Threads die Instanz gleichzeitig erstellen:

public sealed class Singleton
{
    private static Singleton _instance;
    private static readonly object _lock = new object();

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = new Singleton();
                }
                return _instance;
            }
        }
    }
}

Dies ist thread-sicher, aber jeder Aufruf von Instance erwirbt die Sperre, was einen Leistungsaufwand verursacht, selbst nachdem die Instanz erstellt wurde.

Implementierung 3: Double-Check Locking

Double-Check Locking reduziert den Overhead, indem die Sperre nur erworben wird, wenn die Instanz noch nicht erstellt wurde:

public sealed class Singleton
{
    private static volatile Singleton _instance;
    private static readonly object _lock = new object();

    private Singleton() { }

    public static Singleton Instance
    {
        get
        {
            if (_instance == null)
            {
                lock (_lock)
                {
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }
}

Das Schlüsselwort volatile stellt sicher, dass die Zuweisung an _instance abgeschlossen ist, bevor die Variable von anderen Threads zugegriffen wird. Die äußere null-Prüfung vermeidet die Sperre, sobald die Instanz existiert. Die innere null-Prüfung behandelt die Race Condition zwischen zwei Threads, die beide die äußere Prüfung bestanden haben.

Implementierung 4: Statische Initialisierung (eager)

Die .NET-Laufzeitumgebung garantiert, dass ein statischer Konstruktor genau einmal pro Anwendungsdomäne ausgeführt wird, wodurch dieser Ansatz inhärent thread-sicher ist:

public sealed class Singleton
{
    private static readonly Singleton _instance = new Singleton();

    // Explicit static constructor tells the runtime
    // not to mark the type with beforefieldinit
    static Singleton() { }

    private Singleton() { }

    public static Singleton Instance => _instance;
}

Die Instanz wird erstellt, wenn die Klasse zum ersten Mal referenziert wird. Der explizite statische Konstruktor verhindert, dass die Laufzeitumgebung die beforefieldinit-Optimierung verwendet, die dazu führen könnte, dass die Instanz früher als erwartet erstellt wird.

Kompromiss: Die Instanz wird erstellt, sobald auf ein statisches Mitglied zugegriffen wird, selbst wenn Instance nie aufgerufen wird. Für die meisten Anwendungen ist das kein Problem.

Implementierung 5: Lazy (empfohlen)

Die Klasse Lazy<T>, eingeführt in .NET 4, bietet die sauberste thread-sichere verzögerte Initialisierung:

public sealed class Singleton
{
    private static readonly Lazy<Singleton> _lazy =
        new Lazy<Singleton>(() => new Singleton());

    private Singleton() { }

    public static Singleton Instance => _lazy.Value;
}

Lazy<T> verwaltet die gesamte Thread-Synchronisation intern. Die Instanz wird erst beim ersten Zugriff auf Instance erstellt. Dies ist der empfohlene Ansatz für die meisten C#-Anwendungen, da er prägnant, korrekt und leicht verständlich ist.

Sie können auch den Thread-Sicherheitsmodus steuern:

// Default: thread-safe (LazyThreadSafetyMode.ExecutionAndPublication)
new Lazy<Singleton>(() => new Singleton());

// Not thread-safe (use only if single-threaded)
new Lazy<Singleton>(() => new Singleton(), isThreadSafe: false);

// Thread-safe, but each thread may create its own instance; only one is kept
new Lazy<Singleton>(() => new Singleton(),
    LazyThreadSafetyMode.PublicationOnly);

Die Singleton-Klasse versiegeln

Alle obigen Beispiele verwenden das Schlüsselwort sealed. Dies verhindert, dass Unterklassen zusätzliche Instanzen durch Vererbung erstellen. Ohne sealed könnte eine abgeleitete Klasse ihren eigenen Konstruktor offenlegen und die Einzelinstanz-Garantie brechen.

Funktionalität hinzufügen

Ein Singleton kann Zustand halten und Methoden bereitstellen wie jede andere Klasse:

public sealed class AppSettings
{
    private static readonly Lazy<AppSettings> _lazy =
        new Lazy<AppSettings>(() => new AppSettings());

    public static AppSettings Instance => _lazy.Value;

    public string ConnectionString { get; private set; }
    public int MaxRetries { get; private set; }

    private AppSettings()
    {
        // Load settings from configuration file
        ConnectionString = "Server=.;Database=MyApp;Trusted_Connection=true;";
        MaxRetries = 3;
    }

    public void Reload()
    {
        // Re-read settings from configuration file
    }
}

Verwendung:

var connStr = AppSettings.Instance.ConnectionString;

Warum Sie Alternativen in Betracht ziehen sollten

Obwohl Singletons reale Probleme lösen, haben sie bekannte Nachteile:

  • Versteckte Abhängigkeiten. Code, der Singleton.Instance aufruft, hat eine implizite Abhängigkeit, die nicht in seiner Konstruktorsignatur sichtbar ist.
  • Schwer zu testen. Sie können nicht einfach eine Mock- oder Fake-Implementierung in Unit-Tests einsetzen.
  • Globaler veränderlicher Zustand. Wenn der Singleton veränderlichen Zustand hält, kann er in nebenläufigen Anwendungen schwer zu debuggende Probleme einführen.
  • Lebensdauerverwaltung. Der Singleton lebt für die gesamte Anwendungslebensdauer, was möglicherweise nicht für alle Ressourcen angemessen ist.

Bevorzugte Alternative: Dependency Injection

Moderne .NET-Anwendungen verwenden typischerweise einen Dependency-Injection-Container (DI), um Objektlebensdauern zu verwalten. Sie können jede Klasse mit einer Singleton-Lebensdauer registrieren:

// In Program.cs or Startup.cs
builder.Services.AddSingleton<IAppSettings, AppSettings>();

Dann injizieren Sie sie über den Konstruktor:

public class OrderService
{
    private readonly IAppSettings _settings;

    public OrderService(IAppSettings settings)
    {
        _settings = settings;
    }
}

Dieser Ansatz bietet Ihnen dasselbe Einzelinstanz-Verhalten, während die Abhängigkeiten explizit bleiben, Interface-basiertes Design unterstützt wird und einfache Unit-Tests mit Mocks ermöglicht werden.

Vergleichstabelle

AnsatzThread-sicherVerzögertKomplexitätEmpfohlen
Grundlegend (ohne Lock)NeinJaNiedrigNein
Einfaches LockJaJaNiedrigFür einfache Fälle
Double-Check LockingJaJaMittelLegacy-Code
Statische InitialisierungJaNeinNiedrigWenn sofortige Erstellung akzeptabel ist
Lazy<T>JaJaNiedrigJa
DI-ContainerJaKonfigurierbarNiedrigAm besten für moderne Apps

Zusammenfassung

Für die meisten C#-.NET-Anwendungen ist der Lazy<T>-Ansatz die beste eigenständige Singleton-Implementierung: Er ist thread-sicher, verzögert und leicht lesbar. In modernen Anwendungen, die Dependency Injection verwenden, sollten Sie Ihren Dienst jedoch bevorzugt mit AddSingleton<T>() im DI-Container registrieren. Dies bietet Ihnen dieselbe Einzelinstanz-Garantie mit den zusätzlichen Vorteilen expliziter Abhängigkeiten, Testbarkeit und sauberer Architektur.