EMZETT.
Login

Kurz: null bedeutet “kein Objekt”. Der Zugriff darauf löst eine NullReferenceException aus, den berühmtesten Fehler der Programmierung. Seit C# 8 helfen nullable reference types.

Teil des Kurses C#

Kapitel 10 von 16 im Kurs C# (Abschnitt „Robuster Code“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Null-Referenzen

null bedeutet “kein Objekt”. Der Zugriff darauf löst eine NullReferenceException aus, den berühmtesten Fehler der Programmierung. Seit C# 8 helfen nullable reference types: Der Compiler unterscheidet string (nie null) und string? (kann null sein) und warnt bei Risiken.

string? eingabe = null;
string name = eingabe ?? "Gast";                           // Ersatzwert
int laenge = eingabe?.Length ?? 0;                         // sicherer Zugriff
Console.WriteLine($"{name} {laenge}");
 
if (eingabe is not null) Console.WriteLine(eingabe.ToUpper());   // Compiler weiß: hier nicht null
else Console.WriteLine("keine Eingabe");
 
string sicher = eingabe!;                                  // ! = "ich weiß es besser" (Warnung unterdrücken, Vorsicht!)
Console.WriteLine(sicher is null);
 
ArgumentNullException.ThrowIfNull(name);                   // Parameter prüfen
try { Pruefe(null!); } catch (ArgumentNullException e) { Console.WriteLine(e.ParamName); }
 
void Pruefe(string text) { ArgumentNullException.ThrowIfNull(text); }

Ausgabe:

Gast 0
keine Eingabe
True
text

Aktiviert wird das Feature mit <Nullable>enable</Nullable> in der Projektdatei (in neuen Projekten Standard). Nutze den !-Operator nur, wenn du es sicher weißt.

Ausnahmen

try
{
    Console.WriteLine("vorher");
    int x = int.Parse("abc");
    Console.WriteLine("wird nie erreicht " + x);
}
catch (FormatException e)
{
    Console.WriteLine("Ungültige Zahl: " + e.Message);
}
finally
{
    Console.WriteLine("läuft immer");
}
Console.WriteLine("Programm läuft weiter");

Ausgabe:

vorher
Ungültige Zahl: The input string 'abc' was not in a correct format.
läuft immer
Programm läuft weiter

Mehrere catch-Blöcke und Filter

foreach (int variante in new[] { 0, 1, 2, 3, 4 })
{
    try { Teste(variante); }
    catch (DivideByZeroException) { Console.WriteLine("Division durch 0"); }
    catch (IndexOutOfRangeException e) { Console.WriteLine("Index: " + e.Message); }
    catch (ArgumentException e) when (e.ParamName == "wert") { Console.WriteLine("Argument wert"); }
    catch (Exception e) { Console.WriteLine("Sonstiges: " + e.GetType().Name); }
}
 
void Teste(int v)
{
    int zero = 0;
    switch (v)
    {
        case 0: Console.WriteLine(1 / zero); break;
        case 1: { int[] feld = new int[2]; Console.WriteLine(feld[v + 5]); break; }
        case 2: throw new ArgumentException("kaputt", "wert");
        case 3: throw new InvalidOperationException("Zustand");
        default: Console.WriteLine("ok"); break;
    }
}

Ausgabe:

Division durch 0
Index: Index was outside the bounds of the array.
Argument wert
Sonstiges: InvalidOperationException
ok

Die Reihenfolge ist wichtig: spezifisch vor allgemein. Mit when filterst du nach Bedingung.

Eigene Ausnahmen

try
{
    Console.WriteLine(Abheben(100, 30));
    Console.WriteLine(Abheben(100, 130));
}
catch (GuthabenException e)
{
    Console.WriteLine($"{e.Message}, es fehlen {e.Fehlbetrag}");
}
 
decimal Abheben(decimal stand, decimal betrag)
    => betrag > stand ? throw new GuthabenException(betrag - stand) : stand - betrag;
 
class GuthabenException(decimal fehlbetrag) : Exception("Nicht genug Guthaben")
{
    public decimal Fehlbetrag { get; } = fehlbetrag;
}

Ausgabe:

70
Nicht genug Guthaben, es fehlen 30

Ausnahmen weiterreichen

try { Aeussere(); }
catch (Exception e) { Console.WriteLine($"{e.Message} <- {e.InnerException?.Message}"); }
 
void Aeussere()
{
    try { int.Parse("x"); }
    catch (FormatException e)
    {
        throw new InvalidOperationException("Konfiguration ungültig", e);     // Ursache mitgeben
    }
}
 
void Weiter()
{
    try { Aeussere(); }
    catch (Exception) { Console.WriteLine("log"); throw; }                   // throw; bewahrt den Stacktrace
}

Ausgabe:

Konfiguration ungültig <- The input string 'x' was not in a correct format.

Schreibe throw; (nicht throw e;), wenn du eine Ausnahme nach dem Loggen weiterwirfst: Sonst geht der ursprüngliche Stacktrace verloren.

Der Stacktrace

Teilen(1, 0);
 
int Teilen(int a, int b) => a / b;

Fehlermeldung:

Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.

Guter Umgang

  • Nur fangen, was du behandeln kannst; kein leeres catch
  • Erwartbare Fälle mit TryParse, TryGetValue, Result-Typen statt Ausnahmen lösen (Ausnahmen sind langsam)
  • Parameter am Methodenanfang prüfen (ArgumentNullException.ThrowIfNull, ArgumentOutOfRangeException.ThrowIfNegative)
  • Ressourcen mit using freigeben
  • Gib Ausnahmen aussagekräftige Meldungen und die Ursache (innerException)

Merke

  • Nullable Reference Types: string? kann null sein; ??, ?., is not null behandeln es
  • try/catch/finally; spezifische catch-Blöcke vor allgemeinen; when filtert
  • Eigene Ausnahmen erben von Exception
  • throw; bewahrt den Stacktrace; Ursache über innerException
  • TryXyz-Methoden vermeiden Ausnahmen im Normalfall

Übungsaufgabe

Schreibe Wurzel(double x), die bei negativer Zahl eine ArgumentOutOfRangeException wirft, und TryWurzel(double x, out double ergebnis).

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Records, Structs, Enums und Pattern Matching

Weiter: Generics und LINQ

Alle Kapitel: C# im Überblick