Singleton ist das Pattern, das jeder zuerst lernt und am längsten falsch anwendet. Fangen wir trotzdem beim Handwerk an: Wenn es davon wirklich nur eine geben soll, wie schreibt man das in C#?
Die Aufgabe hat zwei Teile: Erzeugung verhindern und eine Instanz anbieten.
public sealed class Konfiguration
{
private static Konfiguration _instanz;
private Konfiguration() { /* teures Laden */ }
public static Konfiguration Instanz
{
get
{
if (_instanz == null) // ← hier
_instanz = new Konfiguration();
return _instanz;
}
}
}Diese Variante ist kaputt
Zwei Threads können die markierte Zeile gleichzeitig erreichen, beide finden null vor, beide erzeugen eine Instanz. Es gibt dann zwei — und je nachdem, wer zuletzt zuweist, arbeiten Teile des Programms mit der einen, Teile mit der anderen. Das ist die Art Fehler, die unter Last auftritt und im Test nie.
Die Variante, die man früher lernte
Historisch behalf man sich mit Double-Checked Locking: einmal ohne Sperre prüfen, um im Normalfall keine zu nehmen, dann gesperrt noch einmal prüfen.
private static volatile Konfiguration _instanz;
private static readonly object _schloss = new object();
public static Konfiguration Instanz
{
get
{
if (_instanz == null)
{
lock (_schloss)
{
if (_instanz == null)
_instanz = new Konfiguration();
}
}
return _instanz;
}
}
Das funktioniert — mit volatile, ohne das der Compiler die Prüfungen umordnen darf. Aber es sind zehn Zeilen, die man falsch abtippen kann, und die Sprache bietet inzwischen zwei kürzere Wege.
Der statische Konstruktor
Die Laufzeit garantiert, dass ein Typinitialisierer genau einmal und threadsicher läuft. Das ist kein Trick, sondern eine Zusage der CLR:
public sealed class Konfiguration
{
public static readonly Konfiguration Instanz = new Konfiguration();
// Ein expliziter statischer Konstruktor unterdrückt "beforefieldinit"
// und legt damit fest: initialisiert wird beim ersten Zugriff auf den Typ.
static Konfiguration() { }
private Konfiguration() { }
}Lazy<T> — die heutige Standardantwort
Lazy<T> kapselt genau dieses Problem: einmalige, threadsichere Initialisierung beim ersten Zugriff. Ein Feld, eine Zeile:
public sealed class Konfiguration
{
private static readonly Lazy<Konfiguration> _instanz =
new Lazy<Konfiguration>(() => new Konfiguration());
public static Konfiguration Instanz => _instanz.Value;
private Konfiguration()
{
// wird garantiert genau einmal ausgeführt
}
}Die Standardeinstellung ist die sichere
Lazy<T> benutzt ohne weitere Angabe LazyThreadSafetyMode.ExecutionAndPublication: Die Factory läuft genau einmal, alle anderen Threads warten und bekommen dasselbe Ergebnis. Es gibt schnellere Modi — PublicationOnly lässt mehrere Threads erzeugen und behält den ersten Wert. Nimm sie nur, wenn du weisst, warum.
Ein Detail, das teuer wird
Wirft der Konstruktor beim ersten Zugriff eine Exception, wird sie bei ExecutionAndPublication zwischengespeichert. Jeder weitere Zugriff auf Instanz wirft dieselbe Ausnahme erneut, ohne den Konstruktor noch einmal zu versuchen.
Bei einer Konfiguration, die beim Start eine Datei liest, heisst das: Ein einziger vorübergehender Lesefehler macht das Objekt für die gesamte Prozesslaufzeit unbrauchbar. Ein Neustart hilft, ein erneuter Versuch nicht.
Das ist ein guter Zeitpunkt für die Frage, die die nächste Lektion stellt: Muss dieses Objekt wirklich ein Singleton sein — oder ist es nur bequem?