Menu

C# Nullable: int?, ?? Null-Sammeloperator, ?. und Null-Prüfungen

Alles über null in C#: Nullable-Werttypen (int?, Nullable<T>) mit HasValue, Value und GetValueOrDefault, die Null-Sammeloperatoren ?? und ??=, der Null-bedingte Operator ?., Null-Prüfungen und Nullable-Referenztypen (string?) aus C# 8.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

null bedeutet „kein Wert“. Referenztypen (Klassen, Strings, Arrays) konnten ihn schon immer enthalten. Werttypen (int, decimal, bool, DateTime, Structs) können es nicht, außer du machst sie mit ? nullable. C# gibt dir dann eine kleine Reihe von Operatoren für Werte, die fehlen könnten: ??, ??= und ?..

Nullable-Werttypen: int?

Hänge ? an einen Werttyp an, um null zu erlauben. int? ist die Kurzform des Structs Nullable<int>, der einen Wert plus ein Flag enthält, das sagt, ob der Wert vorhanden ist:

Ausgabe:

False
True
True
5.50
0
3
Rated 4 stars
4

Typische Quellen für Nullable-Werttypen sind optionale Formularfelder, Query-Parameter und NULL-Spalten in einer Datenbank: Eine Order mit DateTime? ShippedAt ist nicht versandt, solange der Wert null ist. Die Alternative, ein magischer Wert wie -1 oder DateTime.MinValue, sieht aus wie echte Daten und wird früher oder später als solche verwendet.

Ein int? lässt sich nur explizit in int umwandeln, weil die Umwandlung scheitern kann. Das führt zu der einen Exception, die Nullable-Werttypen werfen.

„Nullable object must have a value“

.Value zu lesen, wenn es keinen Wert gibt, oder ein null seiendes int? in int zu casten, wirft InvalidOperationException:

Ausgabe:

InvalidOperationException: Nullable object must have a value.
0
0
not stocked

stock is int count ist ein Muster: Es prüft auf einen Wert und packt ihn in einem Schritt in count aus, was sich besser liest als HasValue plus Value. Die Seite zu Pattern Matching hat weitere Formen.

Arithmetik und Vergleiche mit null

Operatoren auf Nullable-Werttypen sind geliftet: Ist einer der Operanden null, ist das Ergebnis null. Ordnungsvergleiche (<, >, <=, >=) sind die Ausnahme: Sie geben false statt null zurück. (Die andere Ausnahme ist bool? mit & und |, wo null & false gleich false ist.) Zum Beispiel:

Ausgabe:

True
[]
False
False
True
True

Sowohl a > 5 als auch a <= 5 sind false, !(a > 5) ist also nicht dasselbe wie a <= 5, wenn a null ist. Code wie if (!(age >= 18)) Deny(); behandelt ein fehlendes Alter als minderjährig, was gewollt sein kann oder nicht. Entscheide zuerst, was null bedeuten soll, meist mit ??, und vergleiche dann.

Der Null-Sammeloperator ??

a ?? b ergibt a, wenn a nicht null ist, und sonst b. Er funktioniert mit Nullable-Werttypen und mit Referenztypen und lässt sich von links nach rechts verketten:

Ausgabe:

dark
16px
20
ANONYMOUS

Die rechte Seite wird nur bei Bedarf ausgewertet, cache ?? LoadFromDatabase() fragt die Datenbank also nicht ab, wenn der Cache einen Wert hat. ?? nimmt auch einen throw-Ausdruck an, was eine kompakte Absicherung ergibt (siehe throw):

this.repository = repository ?? throw new ArgumentNullException(nameof(repository));

??= (C# 8)

x ??= y weist x den Wert y nur zu, wenn x null ist. Das ist die Redewendung für verzögerte Initialisierung:

private List<string> tags;

public void AddTag(string tag)
{
    tags ??= new List<string>();   // C# 8 and later
    tags.Add(tag);
}

Vor C# 8 schreibst du tags = tags ?? new List<string>(); oder if (tags == null) tags = new List<string>();.

Der Null-bedingte Operator ?.

a?.Member wertet Member nur aus, wenn a nicht null ist; sonst ist der ganze Ausdruck null. Er macht aus einer Kette von Null-Prüfungen einen einzigen Ausdruck:

Ausgabe:

Braga
(no city)
(no customer)
0
True
handler: saved

Details, die man kennen sollte:

  • Der Operator schließt den Rest der Kette kurz: In nobody?.Address?.City läuft nichts rechts davon, sobald nobody null ist.
  • Gibt der Member einen Werttyp zurück, wird das Ergebnis nullable: Orders?.Count ist int?, deshalb braucht es ?? 0, um wieder ein int zu werden.
  • ?. kann vor C# 14 nicht das Ziel einer Zuweisung sein: customer?.Name = "x" kompiliert in früheren Versionen nicht.
  • handler?.Invoke(...) ist der Standardweg, ein Event auszulösen oder einen optionalen Callback aufzurufen.

Nimm ?. dort, wo null ein legitimer Zustand ist. Es überall zu verstreuen, um Exceptions zu vermeiden, versteckt den Bug, der das unerwartete null erzeugt hat, und das Programm läuft mit fehlenden Daten weiter, statt dort zu scheitern, wo das Problem liegt.

Auf null prüfen

if (customer == null) { ... }             // classic
if (customer is null) { ... }             // C# 7 constant pattern
if (customer != null) { ... }
if (customer is not null) { ... }         // C# 9
if (customer is { } c) { ... }            // C# 8 property pattern: not null, and named

is null führt immer eine echte Null-Prüfung durch, während == die Operatorüberladung des Typs aufruft, falls es eine gibt, eine schlecht geschriebene Überladung kann x == null also lügen lassen. Bei Argumenten, die nicht null sein dürfen, scheitere früh:

public void Ship(Order order)
{
    ArgumentNullException.ThrowIfNull(order);   // .NET 6+
    // ...
}

Nullable-Referenztypen (C# 8)

Ein ? an einem Werttyp ändert den Typ. Ein ? an einem Referenztyp ändert seit C# 8 nur, was der Compiler prüft. Mit eingeschaltetem Feature (<Nullable>enable</Nullable> in der Projektdatei, der Standard für neue Projekte seit .NET 6) bedeutet string „sollte nie null sein“ und string? „kann null sein“, und der Compiler warnt, wenn der Code nicht dazu passt:

#nullable enable

public class User
{
    public string Email { get; set; }      // warning CS8618: Non-nullable property 'Email'
                                           // must contain a non-null value when exiting constructor.
    public string? Nickname { get; set; }  // allowed to be null

    public int NicknameLength() => Nickname.Length;
    // warning CS8602: Dereference of a possibly null reference.

    public int SafeLength() => Nickname?.Length ?? 0;   // no warning
}

string? input = Console.ReadLine();
string name = input;                       // warning CS8600: Converting null literal or possible
                                           // null value to non-nullable type.
string checkedName = input ?? "guest";     // no warning
int len = input!.Length;                   // ! tells the compiler "trust me": no warning

Der Compiler folgt deinen Prüfungen durch die Methode: Nach if (input != null) gilt input als nicht null. Der Operator ! (null-forgiving) bringt eine Warnung zum Schweigen, wenn du es besser weißt als die Analyse; jeder ist eine Behauptung, die der Compiler nicht prüfen kann, halte sie also selten.

Zwei Fakten klären die meiste Verwirrung. Erstens sind das nur Warnungen: Das Programm läuft genauso, und ein string kann zur Laufzeit trotzdem null enthalten, wenn er aus Code ohne Annotationen kommt. Zweitens ist string? nicht Nullable<string>; es hat kein HasValue.

Häufige Fehler

  • .Value ohne Prüfung. Wirft InvalidOperationException, wenn null. Nimm ??, GetValueOrDefault() oder is int n.
  • Annehmen, dass !(x > 5) dasselbe wie x <= 5 bedeutet. Nicht, wenn x null ist: Jeder Ordnungsvergleich mit null ist false.
  • Jedes null mit ?. abdecken. Das bringt das Symptom zum Schweigen und schiebt den Fehler weiter von der Ursache weg.
  • Warnungen zu Nullable-Referenztypen als Rauschen behandeln. Jede markiert eine Zeile, die NullReferenceException werfen kann. Korrigiere den Ablauf oder drücke die Absicht mit ? aus.
  • Magische Werte statt null. -1, 0 und DateTime.MinValue für „unbekannt“ sehen aus wie echte Daten. Nimm int? und DateTime?.

Häufig gestellte Fragen

Was bedeutet int? in C#?

int? ist die Kurzform von Nullable<int>: ein int, das auch null sein kann. Werttypen wie int, decimal, bool und DateTime enthalten normalerweise immer einen Wert; ein ? gibt ihnen einen Zustand „kein Wert“, den ein fehlendes Formularfeld oder eine NULL-Spalte in einer Datenbank braucht. Prüfe ihn mit HasValue oder != null und lies ihn mit Value oder GetValueOrDefault().

Was macht ?? in C#?

a ?? b ist der Null-Sammeloperator: Er ergibt a, wenn a nicht null ist, sonst b. string name = input ?? "guest"; liefert einen Ersatzwert in einem Ausdruck. Er lässt sich verketten (a ?? b ?? c nimmt den ersten Wert, der nicht null ist), und b wird nur bei Bedarf ausgewertet. ??= (C# 8) weist die rechte Seite nur zu, wenn die Variable null ist.

Was macht ?. in C#?

a?.B ist der Null-bedingte Operator: Ist a null, ist der ganze Ausdruck null, und B wird nie ausgewertet, also keine NullReferenceException. Er lässt sich verketten (order?.Customer?.Address?.City) und funktioniert bei Methodenaufrufen (logger?.Log(...)), Events (Changed?.Invoke(...)) und Indexern (items?[0]). Kombiniere ihn mit ?? für einen Standardwert: order?.Customer?.Name ?? "unknown".

Was verursacht „Nullable object must have a value“?

.Value eines Nullable-Werttyps zu lesen, der null ist, oder ihn in den nicht nullable Typ zu casten ((int)maybe), wirft eine InvalidOperationException mit dieser Meldung. Prüfe zuerst HasValue oder nimm GetValueOrDefault(), ?? fallback oder ein Muster: if (maybe is int n) { ... }.

Wie prüfe ich in C# auf null?

x == null und x is null funktionieren beide; is null (C# 7) kann nicht von einem Typ beeinflusst werden, der == überlädt, deshalb bevorzugen es viele Codebasen. Die Verneinung ist x != null oder in C# 9 x is not null. Um null-Argumente abzulehnen, nimm ArgumentNullException.ThrowIfNull(x) (.NET 6) oder in älterem Code _ = x ?? throw new ArgumentNullException(nameof(x));.

Was sind Nullable-Referenztypen in C#?

Ein Compiler-Feature aus C# 8, eingeschaltet mit <Nullable>enable</Nullable> (der Standard in neuen Projekten ab .NET 6). Ist es an, bedeutet string „nie null“ und string? „kann null sein“, und der Compiler warnt, wenn du möglicherweise null dereferenzierst (CS8602) oder null einer nicht nullable Variable zuweist (CS8600). Es ändert nur Warnungen, nicht das Verhalten zur Laufzeit.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S