Tony Hoare, der die null-Referenz 1965 erfunden hat, nannte sie später seinen «Milliarden-Dollar-Fehler». Der Grund ist nicht null selbst — es ist, dass sie in keiner Signatur auftaucht.
public Kunde FindeKunde(int id) { … }
// Der Aufrufer:
var kunde = FindeKunde(42);
Console.WriteLine(kunde.Name); // knallt irgendwann. Aber wann?Die Signatur verspricht einen Kunde. Ob sie manchmal null liefert, steht — wenn überhaupt — in einem Kommentar. Der Aufrufer erfährt es aus einer NullReferenceException in der Produktion.
Die eingebaute Antwort: Nullable Reference Types
Seit C# 8 kann der Compiler diese Frage beantworten. Aktiviert wird das im Projekt, nicht pro Datei:
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>Danach ist Kunde ein Versprechen und Kunde? eine Warnung:
public Kunde? FindeKunde(int id) { … } // kann leer ausgehen — steht in der Signatur
public Kunde HoleKunde(int id) { … } // wirft, wenn es ihn nicht gibt
var kunde = FindeKunde(42);
Console.WriteLine(kunde.Name); // CS8602: mögliche Null-Referenz
if (kunde is not null)
Console.WriteLine(kunde.Name); // in OrdnungEs sind nur Warnungen
Nullable Reference Types ändern nichts zur Laufzeit — sie sind reine Compilerprüfung, und eine Bibliothek ohne Annotationen liefert weiterhin null. Ohne TreatWarningsAsErrors (oder mindestens WarningsAsErrors=nullable) werden die Meldungen erfahrungsgemäss ignoriert, und dann bringt der Schalter nichts.
Die Namenskonvention, die es klarmacht
Unabhängig vom Compiler hat sich in .NET ein Paar durchgesetzt, das man beim Lesen sofort versteht:
| Präfix | Bedeutung | Bei Nichtvorhandensein |
|---|---|---|
Find… | Suchen, Fehlen ist normal | liefert null |
Get…, Hole… | Holen, Fehlen ist ein Fehler | wirft eine Ausnahme |
Try… | Versuchen, Fehlen ist normal | liefert false |
LINQ hält sich daran: Single() wirft, SingleOrDefault() liefert null. First() wirft, FirstOrDefault() nicht.
Niemals null bei Sammlungen
Für Sammlungen gibt es keine Diskussion:
Liefert `null`
public List<Rechnung> HoleOffene(int id)
{
var offene = _db.Rechnungen
.Where(r => r.KundeId == id
&& !r.Bezahlt)
.ToList();
return offene.Count > 0
? offene
: null; // warum?
}
// Jeder Aufrufer:
var r = HoleOffene(42);
if (r != null)
foreach (var x in r) …Liefert leer
public IReadOnlyList<Rechnung>
HoleOffene(int id)
{
return _db.Rechnungen
.Where(r => r.KundeId == id
&& !r.Bezahlt)
.ToList();
}
// Jeder Aufrufer:
foreach (var x in HoleOffene(42)) …Eine leere Liste ist die richtige Antwort auf «keine offenen Rechnungen». null zwingt jeden Aufrufer zu einer Abfrage, die nichts bedeutet.
Argumente prüfen — an der Grenze
Bei öffentlichen Methoden lohnt sich die Prüfung am Eingang. Seit .NET 6 in einer Zeile:
public void Verbuche(Rechnung rechnung, string referenz)
{
ArgumentNullException.ThrowIfNull(rechnung);
ArgumentException.ThrowIfNullOrWhiteSpace(referenz);
…
}Warum das besser ist als die spätere NullReferenceException
Die ArgumentNullException nennt den Parameternamen und entsteht an der Grenze, an der der falsche Wert hereinkam. Eine NullReferenceException entsteht drei Methoden tiefer und sagt nur, dass irgendwo etwas null war.
Kurz nachgedacht
Zählt nicht — nur zum Prüfen, ob es angekommen ist.
1.Was ändern Nullable Reference Types zur Laufzeit?