Blog
Fangen Sie an, mit nullbaren Referenztypen zu arbeiten!

Seit C# 8 können sich Entwickler für die Verwendung von Nullable Reference Types (NRTs) entscheiden. Mit NRTs erhalten Entwickler eine Reihe von 'Werkzeugen', um die häufigste Ausnahme zu vermeiden: die NullReferenceException. Zu diesen Werkzeugen gehören die nullable Annotations, Attribute, Warnungen und die statische Codeanalyse. Ab .NET 6 sind die NRTs standardmäßig aktiviert, wenn Sie ein neues Projekt erstellen. Sie können sich jederzeit dagegen entscheiden, aber - um ehrlich zu sein - ich denke, das sollten Sie nicht!
Und obwohl es NRTs schon seit einiger Zeit gibt, finde ich, dass viele Menschen immer noch zögern, sie anzunehmen. Vielleicht ist unbekannt ungeliebt? Ich bin fest davon überzeugt, dass die vollständige Übernahme von NRTs zu besserem, verständlicherem und robusterem Code führt! Lassen Sie mich Ihnen zeigen, was ich damit meine und wie Sie anfangen können, NRTs zu verwenden.
Es geht nur darum, Absichten auszudrücken
Einer der Hauptgründe, warum NRTs großartig sind, ist, dass sie Ihrem Code viel mehr Bedeutung verleihen. Sie machen den Code weniger zweideutig und verständlicher.
Wert-Typen
Die Möglichkeit, explizit anzugeben, ob ein Wertetyp nullbar ist oder nicht, ist etwas, an das wir uns so sehr gewöhnt haben. Die Absicht bei der Verwendung eines Wertetyps ist klar: Es wird immer einen Wert geben, den man erwarten oder bereitstellen kann, er kann nicht null sein. Nullbare Wertetypen hingegen können in Szenarien verwendet werden, in denen es völlig legitim ist, dass ein Wert Null sein könnte, dass ein Wert optional ist; es gibt Anwendungsfälle, in denen es sinnvoll ist, keinen Wert zu haben.
Referenz-Typen
Dank Tony Hoare's "The billion dollar mistake"(Tony Hoare - Wikipedia ) können Referenztypen null sein. Was noch schlimmer ist, ist die Tatsache, dass in .NET Werte von Referenztypen standardmäßig null sind und es vor C# 8 keine Möglichkeit gab, anzugeben, dass eine Instanz eines Referenztyps nicht null sein sollte. In früheren Versionen von .NET gab es Code Contracts, mit deren Hilfe man definieren konnte, was nullbar sein sollte und was nicht. Aber Code Contracts haben ausgedient und werden durch NRTs ersetzt. Die häufigste Art und Weise, wie Entwickler mit der Nullbarkeit umgehen, ist die Durchführung von Null-Prüfungen an Stellen, an denen sie keine Null-Werte haben möchten, und das Auslösen einer Exception. Der Nachteil dabei? Es handelt sich um Laufzeitprüfungen! Wenn ein Entwickler einen Nullwert an einer Stelle übergibt, an der er nicht erwartet wird, weiß er erst zur Laufzeit, dass er nicht zulässig ist.
Der folgende Ausschnitt zeigt eine Methodensignatur, die, wenn keine NRTs verwendet werden, viele Unsicherheiten aufweist:
Ist es möglich, einen Nullwert als Suchkriterium zu übergeben, wenn ich die gesamte Liste der Elemente haben möchte? Kann ich null als SortParams übergeben, wenn ich nicht möchte, dass mein Suchergebnis sortiert wird? Wenn wir nichts finden, erhalte ich eine leere Liste zurück, richtig? Oder wird das Ergebnis gleich Null sein? Wenn wir uns die Signatur der Methode ansehen, wissen wir es einfach nicht. Wir wissen nicht mit Sicherheit, was die Absicht dieser Methode und ihrer Parameter ist, oder was die Absicht des Entwicklers war. Oh, und haben Sie bemerkt, dass diese Methode virtuell ist? Eine andere Klasse, die diese Klasse erbt, könnte diese Methode außer Kraft setzen und möglicherweise die Parameter und den Rückgabetyp völlig anders behandeln. Ganz zu schweigen davon, dass dies als schlechte Praxis gilt, da es eindeutig gegen das Liskovsche Substitutionsprinzip verstößt.
Wenn Sie keine NRTs verwenden, gibt es keine Möglichkeit - wie bei (nullbaren) Werttypen -, die Absicht auszudrücken. Ist es erlaubt, einen Nullwert an eine bestimmte Methode zu übergeben? Hat ein Wert, der von einer Methode zurückgegeben wird, immer einen Wert, oder sollte ich eine Null-Prüfung durchführen? Da die Plattform uns nicht dazu zwingt, über die Nullbarkeit nachzudenken, neigen Entwickler dazu, nicht wirklich darüber nachzudenken. Das Ergebnis? Erstens: NullReferenceExceptions! Zweitens: jede Menge Code, bei dem die Absicht nicht immer klar ist.
Mit NRTs sind Sie gezwungen, die Nullbarkeit zu berücksichtigen! Dadurch wird Ihr Code viel selbstbeschreibender: Es geht darum, Absichten auszudrücken und zu definieren, was erwartet wird, was möglich ist und was nicht.
Wenn Sie mit NRTs arbeiten, wird die oben beschriebene Suchmethode viel eindeutiger:
Beide Argumente können nullbar sein, und wir können sicher sein, dass wir von dieser Methode niemals einen Nullwert zurückbekommen werden. Wenn wir uns die Signatur der Methode ansehen, können wir sofort ihren Verwendungszweck erkennen.
Packen wir es an!
Seit C# 8 können wir einen nullable annotations Kontext aktivieren, um NRTs zu verwenden. Mit NRTs müssen Entwickler explizit angeben, ob ein Wert eines Referenztyps null sein kann, da sie standardmäßig nicht-nullbar sind. Um NRTs für Ihr gesamtes Projekt zu aktivieren, fügen Sie Folgendes zu Ihrer csproj-Datei hinzu:
Sie können die NRTs auch auf Dateibasis aktivieren oder deaktivieren, indem Sie die folgenden Direktiven verwenden:
löschbar aktivieren
Oder
löschbar deaktivieren
Sobald diese Funktion aktiviert ist, kann ein löschbarer Referenztyp definiert werden, indem der Deklaration ein '?' hinzugefügt wird, analog zu den Wertetypen:
Anders als bei nullbaren Werttypen ändern wir den zugrunde liegenden Typ nicht, indem wir ein '?' hinzufügen: Sowohl 'string' als auch 'string?' verweisen auf die Klasse System.String. Der Compiler behält die Annotation nullable im Auge, aber der Typ selbst ändert sich nicht! Mit diesen zusätzlichen Informationen kann der Compiler eine sehr interessante und hilfreiche Codeanalyse durchführen. Werfen Sie einen Blick auf das folgende Listing:
Sehen Sie, wie der Compiler sofort anfängt, uns Warnungen zu geben. Die Variable text ist nullbar, so dass wir vor dem Aufruf einer Methode eine Nullprüfung durchführen sollten, anders als bei text2, das nicht nullbar ist. Aber weil text2 nicht nullbar ist, erhalten wir eine Warnung, wenn wir ihr null zuweisen.
Ignorieren Sie die Warnungen nicht
Viel zu oft sehe ich, dass Warnungen von Entwicklern ignoriert werden. Ich persönlich aktiviere bei den Projekten, an denen ich arbeite, immer 'Warnungen als Fehler behandeln'. NRTs zu aktivieren, ohne sicherzustellen, dass alle damit zusammenhängenden Warnungen behoben werden, halte ich für nicht sehr sinnvoll. Nicht jeder möchte Warnungen als Fehler behandeln, aber wussten Sie, dass Sie den Compiler anweisen können, nur NRT-bezogene Warnungen als Fehler zu behandeln? Wenn Sie dies zu Ihrer csproj hinzufügen, tun Sie genau das:
Lassen Sie uns die obige Warnung beheben, indem wir eine explizite Null-Prüfung hinzufügen:
Beachten Sie, dass der Fehler, den wir zuvor bei der Textvariable hatten, jetzt nicht mehr auftritt!
Der Compiler verfolgt ständig den Null-Zustand einer Variablen. In Visual Studio können wir den abgeleiteten Null-Zustand abfragen, indem wir über die Variablen streichen. Der Tooltip zeigt an, ob eine Variable an einer bestimmten Stelle null oder nicht null sein kann. Werfen Sie nun einen Blick auf das Folgende:
Keine Warnung. Und wenn wir den Mauszeiger nach dem ersten if über die Textvariable bewegen, sagt die QuickInfo, dass die Textvariable hier nicht null ist. Woher weiß der Compiler das? Wir führen keine explizite Null-Prüfung durch, aber der Analysator schließt trotzdem auf den Null-Status der Variablen, nachdem er die IsNullOrWhiteSpace-Methode aufgerufen hat.
Die Antwort lautet:
Attribute
Es gibt eine Handvoll Attribute, die wir verwenden können, um dem Compiler zu helfen, den Null-Status von Variablen herauszufinden. Schauen wir uns die Methode IsNullOrWhiteSpace der Klasse String an, die wir im obigen Code verwendet haben:
Das NotNullWhen-Attribut kann verwendet werden, um dem Compiler mitzuteilen, dass das angegebene Argument, das nullbar ist, nicht null sein wird, wenn die Methode den angegebenen boolschen Wert (in diesem Fall false) zurückgibt. Dank dieses Attributs konnte der Compiler in unserem vorherigen Beispiel feststellen, dass der Text innerhalb des if-Blocks nicht null ist, da wir diesen if-Block nur betreten, wenn die Methode IsNullOrWhiteSpace den Wert false zurückgibt. All diese Attribute, die dem Compiler helfen, den Null-Status von Variablen zu ermitteln, können in verschiedene Kategorien unterteilt werden.
Bedingte Nachbedingung
Attribute dieser Kategorie lassen den Compiler anhand des Rückgabewerts einer Methode auf den Nullzustand einer Variablen schließen. Genau wie das NotNullWhen-Attribut, das wir gerade besprochen haben. Ein weiteres Beispiel für ein solches Attribut in dieser Kategorie ist das MaybeNullWhen-Attribut. Dieses wird typischerweise für Try-Methoden verwendet, wie z.B. die TryGetValue-Methode des IDictionary:
Obwohl wir einen nullbaren String-Wert als Parameter übergeben, warnt uns der Compiler innerhalb des if-Blocks nicht, dass der Ergebniswert möglicherweise null ist. Das liegt daran, dass die Definition von TryGetValue wie folgt aussieht:
Aufgrund des Attributs MayBeNullWhen weiß der Compiler, dass der Ausgangswert nur dann null sein kann, wenn die Methode false zurückgibt. Daher erhalten wir im obigen Beispiel keine Warnung beim Zugriff auf die Eigenschaft Length der Ergebnisvariablen, wenn die Methode TryGetValue den Wert true zurückgegeben hat. Ein weiteres Attribut in dieser Kategorie ist das Attribut NotNullWhenNotNull. Ein Beispiel für dieses Attribut finden Sie in der Methode GetFileName von Path:
Dieses Attribut teilt dem Compiler mit, dass der Rückgabewert nicht null sein wird, wenn der angegebene Parameter nicht null ist. Lassen Sie uns dies in Aktion sehen:
Da die Pfadvariable vor der Übergabe an die GetFileName-Methode auf Null geprüft wird, folgert der Compiler dank des Attributs NotNullIfNotNull, dass die resultierende Variable filename nicht Null ist.
Diese Attribute werden für Klassen und Methoden in der BCL ab .NET 3.0 und .NET Standard 2.1 verwendet. Können Sie NRTs in .NET Standard Versionen vor 2.1 verwenden? Ja! Aber Methoden und Klassen haben diese Attribute nicht, um dem Compiler zu helfen, den Null-Status abzuleiten. Sie müssen also selbst zusätzliche Null-Prüfungen durchführen, wenn Sie alle Warnungen loswerden wollen.
Die vorangegangenen Beispiele zeigten Beispiele für Attribute auf eingebaute Methoden und Typen in der BCL. Wir können diese Attribute natürlich auch zu unseren eigenen Methoden hinzufügen. Interessanterweise erhalten Sie bei der Anwendung dieser Attribute Warnungen, wenn die Implementierung der Methode nicht mit dem definierten Attribut übereinstimmt. Werfen Sie einen Blick auf den folgenden Ausschnitt, der ein gutes Beispiel für die Verwendung des Attributs NotNullIfNotNull ist:
Hätte ich einen Tippfehler gemacht und 'is null' statt 'is not null' eingegeben, hätte ich eine Warnung erhalten:
Die Warnung lautet: "Der Rückgabewert darf nicht null sein, da der Parameter 'username' nicht null ist." Mit diesem Tippfehler würde die Implementierung dieser Methode nicht dem entsprechen, was das Attribut NotNullOfNotNull beschreibt.
Nachbedingung
Diese Kategorie von Attributen informiert den Compiler über den Null-Status eines Wertes, nachdem die Methode erfolgreich abgeschlossen wurde. Das NotNull-Attribut wird in der Regel bei nullbaren Methodenparametern in Szenarien verwendet, in denen wir dem Compiler mitteilen wollen, dass der Wert des Parameters nicht null ist, wenn die Methode erfolgreich beendet wird. Wir könnten zum Beispiel die Methode GetProfilePicture umschreiben, so dass sie eine Ausnahme auslöst, wenn der angegebene Parameter username null ist:
Aufgrund des NotNull-Attributs weiß der Compiler, dass die Variable username nicht null sein wird, wenn die Methode erfolgreich abgeschlossen wird.
Die obige Auflistung zeigt, dass der Compiler uns im catch-Block davor warnt, dass der Benutzername null sein kann. Wir sollten eine Null-Prüfung durchführen, bevor wir auf ihn zugreifen. Im Try-Block hingegen warnt uns der Compiler nach dem erfolgreichen Aufruf der Methode GetProfilePicture nicht, da wir zu diesem Zeitpunkt wissen, dass der Wert von Username nicht null sein wird.
Wie Sie vielleicht erwarten, kann das MaybeNull-Attribut für einen nicht-nullbaren Parameter deklariert werden, der per Referenz an eine Methode übergeben wird und der bei der Rückkehr der Methode null sein könnte. Vor C#9 wurde das MaybeNull-Attribut außerdem häufig in Kombination mit Generics verwendet. In C#8 ist es nicht möglich, einen nicht eingeschränkten generischen Typ als nullable zu markieren. Aus diesem Grund war die einzige Möglichkeit, zu markieren, dass ein generischer Rückgabewert ohne Einschränkung null sein könnte, die Verwendung des Attributs MayBeNull.
Nach meiner persönlichen Erfahrung verwende ich das MaybeNull-Attribut nicht oft. Anstatt das Attribut hinzuzufügen, kann ich einfach den Rückgabetyp einer Methode nullable machen, was ich jetzt für jeden Typ tun kann, da C#9 nullable unconstraint generics unterstützt.
Vorbedingung
Neben Conditional Post-Condition und Postcondition gibt es noch Attribute, die die Kategorie Precondition bilden. Mit diesen Attributen können wir einen Wert zuweisen, der nicht mit der Annotation nullable dieser Variablen übereinstimmt.
Stellen Sie sich eine UserProfile-Klasse vor, die eine Bio-Eigenschaft hat. Wir möchten, dass die Bio-Eigenschaft nicht nullbar ist, aber das Hintergrundfeld könnte nullbar sein (dies könnte z.B. der Wert sein, der in der Datenbank gespeichert ist). Wenn das zugrundeliegende Feld Null ist, gibt die Eigenschaft Bio einen Standardwert zurück. Da die Eigenschaft Bio nicht nullbar ist, können wir ihr nicht null zuweisen, ohne eine Warnung zu erhalten. Andererseits möchten wir vielleicht die Bio-Eigenschaft des Benutzers "zurücksetzen", d.h. wir möchten der Bio-Eigenschaft einen Nullwert zuweisen können. Dies lässt sich leicht durch Hinzufügen eines Attributs AllowNull erreichen:
So können wir der Eigenschaft Bio einen Nullwert zuweisen und gleichzeitig sicher sein, dass wir niemals einen Nullwert zurückbekommen:
Das Attribut DisallowNull ist dasselbe, aber anders. Sein Zweck ist es, die Zuweisung eines Nullwerts zu einer löschbaren Eigenschaft zu verbieten. Zum Beispiel kann eine Klasse Product eine Eigenschaft Rating haben, die nullbar ist, d.h. ein Produkt kann noch keine Bewertung haben, aber sobald es eine Bewertung hat, kann sie nicht entfernt, sondern nur aktualisiert werden.
Mit dem DisallowNull-Attribut können wir eine Eigenschaft haben, die Null sein kann, aber vom Code aus dürfen wir ihr keinen Nullwert zuweisen:
Unerreichbarer Code
Ein weiteres Paar interessanter Attribute sind die Attribute DoesNotReturn und DoesNotReturnIf. Ihr Zweck ist es, den Compiler über nicht erreichbaren Code zu informieren. Sie sind vor allem bei Exception-Hilfsmethoden nützlich. Werfen Sie einen Blick auf das folgende Beispiel:
Sehen Sie, dass wir unter dem if-Block keine Warnung erhalten, wenn wir auf die Eigenschaft Category zugreifen. Das liegt daran, dass wir eine Nullprüfung durchführen und die Methode ThrowUnexpectedValueException mit einem DoesNotReturn-Attribut gekennzeichnet ist. Daher kann der Analyzer einwandfrei feststellen, dass es keine andere Möglichkeit gibt, den Code unterhalb des if-Blocks zu erreichen, als wenn die Eigenschaft Category nicht null ist.
Das DoesNotReturnIf-Attribut funktioniert auf die gleiche Weise, aber statt dass die Methode nie zurückkehrt, können wir einen bool-Parameter angeben, ob die Methode zurückkehrt oder nicht. Wenn der Wert des Arguments des Attributs mit dem Wert des zugehörigen Parameters übereinstimmt, bedeutet dies, dass die Methode nicht zurückkehren wird. Wenn wir das DoesNotReturnIf-Attribut verwenden möchten, können wir unseren Code umstrukturieren und es wie folgt verwenden:
Beide Attribute bewirken dasselbe und haben denselben Effekt, werden aber auf leicht unterschiedliche Weise und in verschiedenen Szenarien eingesetzt.
Hilfsmethoden
Die letzten beiden Attribute, die wir besprechen müssen, sind MemberNotNull und MemberNotNullWhen. Das erste wird typischerweise bei Methoden verwendet, die von einem Konstruktor aus aufgerufen werden, um dem Compiler mitzuteilen, welche Mitglieder die Methode setzt. Wenn Sie ein nicht-nullbares Mitglied nicht vom Konstruktor aus zuweisen, gibt der Compiler eine Warnung aus. Wenn Sie die Zuweisung eines Mitglieds in einer Methode vornehmen, die vom Konstruktor aus aufgerufen wird, kann der Compiler dies leider nicht verfolgen. Indem wir das Attribut MemberNotNull zu einer Methode hinzufügen, können wir dem Compiler mitteilen, welche Felder und Eigenschaften von der Methode zugewiesen werden. Auf diese Weise weiß der Compiler, dass nach dem Aufruf dieser Methode die definierten Eigenschaften und/oder Felder gesetzt sind. Wenn der Entwickler vergisst, eines der aufgeführten Mitglieder innerhalb der Methode zuzuweisen, gibt der Compiler eine Warnung aus.
Dies ist sehr hilfreich bei Hilfsmethoden, die einen bestimmten Zustand des Objekts festlegen.
Mit dem Attribut MemberNotNullWhen können wir dem Compiler mitteilen, dass, wenn eine bestimmte boolesche Eigenschaft oder Methode den angegebenen Wert zurückgibt, der Wert einer anderen Eigenschaft oder eines Feldes nicht null ist. Auf diese Weise können wir Hilfsmethoden erstellen, die einen aussagekräftigen boolschen Wert über den Null-Status einer Eigenschaft zurückgeben, und nach dem Aufruf der Methode den Compiler veranlassen, den Null-Status der besagten Eigenschaft im nachfolgenden Code abzuleiten.
Sehen Sie, wie wir eine Warnung für die Eigenschaft Rating erhalten, wenn die Methode CheckHasRating false zurückgeben würde, während wir keine erhalten, wenn die Methode true zurückgibt. Das ist alles dem Attribut MemberNotNullWhen zu verdanken. Dieses Attribut kann auch auf eine Eigenschaft vom Typ bool angewendet werden:
Abmeldung
In manchen Situationen möchten Sie vielleicht (vorübergehend) auf die Verwendung von NRTs verzichten. Durch Hinzufügen der Direktive '#nullable disable' können wir den Kontext der Annotation 'nullable' deaktivieren und unseren Code wie 'in den alten Tagen' arbeiten lassen, d.h. Referenztypen sind nullable und standardmäßig null. Das heißt, bis zum Ende der Datei oder bis der Compiler auf eine '#nullable enable'- oder '#nullable restore'-Direktive stößt. Dies kann sehr praktisch sein, wenn Sie externen Code aus einem Blogbeitrag oder von StackOverflow kopieren. Online gefundener Code berücksichtigt oft keine NRTs, so dass die Verwendung dieses Codes in Ihrer Codebasis möglicherweise viele Warnungen bezüglich der Nullbarkeit auslösen würde. Um diese Warnungen zu umgehen, sollten Sie den Code mit den bereits erwähnten Direktiven umgeben. In einer idealen Welt würden Sie den Code nach der Validierung refaktorisieren, mit NRTs umgehen und die Direktiven wieder entfernen wollen.
Wir fangen gerade erst an...
Ich hoffe, Sie haben nach der Lektüre dieses Artikels ein gutes Verständnis für den Umgang mit NRTs. Es ist eine Menge zu verdauen, mit neuen Konzepten, die Sie verinnerlichen müssen, und es verlangt von Ihnen als Entwickler, dass Sie auf die Nullbarkeit achten und ausdrucksstärker und expliziter sind. Ehrlich gesagt, habe ich in diesem Artikel bei weitem nicht alles behandelt. Ich habe kaum an der Oberfläche gekratzt. Dinge wie die Deserialisierung von Objekten, die Integration mit Entity Framework, die Sicherstellung, dass Ihre Objekte konstruktionsbedingt korrekt sind, usw. erfordern besondere Aufmerksamkeit! Aber es ist wichtig, mit den Kernkonzepten zu beginnen.
Beachten Sie auch, dass die Arbeit mit NRTs nicht mehr garantiert, dass es keine Nullreferenz-Ausnahme gibt. Ein anderer Entwickler könnte den Null-Vergabe-Operator (!) verwenden, um einem nicht-nullbaren Element null zuzuweisen oder null zu übergeben, wenn der Argumenttyp nicht-nullbar ist. Zumindest weiß der Entwickler in diesem Moment, dass er etwas tut, das höchstwahrscheinlich zu einem Fehler im System führen wird. Und wenn eine Ausnahme auftritt, ist das sein eigener Fehler. Bei der Deserialisierung eines Objekts, das keinen Wert für ein nicht-nullbares Mitglied hat, ist die Sache etwas komplizierter. Deshalb sollten Sie in manchen Fällen trotzdem eine Null-Prüfung durchführen. Vor allem bei API-Endpunkten, um zu sehen, ob der angegebene Wert korrekt ist, was die Nichtnullbarkeit angeht.
Ich persönlich halte NRTs für eine der besten Funktionen, die in den letzten Jahren in .NET eingeführt wurden. Obwohl es eine ziemlich hohe Hürde darstellt, um sie vollständig zu nutzen, macht sie Ihren Code meiner Meinung nach in vielerlei Hinsicht besser. Wenn Sie weitere Fragen zu NRTs haben, wenden Sie sich bitte an uns!
Möchten Sie mehr erfahren? Nehmen Sie Kontakt auf!
Verfasst von

Pieter Nijs
Pieter Nijs is a .NET consultant at Xebia in Belgium, with a keen interest in mobile and cloud development. He's been instrumental in diverse projects, from vast healthcare and telecom systems to compact LOB apps. Now, Pieter is exploring AI's potential to enhance customer projects innovatively. Passionate about technology, he actively experiments and shares knowledge as a conference speaker and trainer. Pieter has been awarded the Microsoft MVP Award since 2017, reflecting his unwavering passion and expertise in serving the community.
Contact