C # 8 und Nullgültigkeit. Wie leben wir damit?

Hallo Kollegen! Es ist Zeit zu erwähnen, dass wir Pläne haben, Ian Griffiths ' grundlegendes Buch über C # 8 zu veröffentlichen:


In der Zwischenzeit hat der Autor in seinem Blog zwei verwandte Artikel veröffentlicht, in denen er die Feinheiten neuer Phänomene wie Nullbarkeit, Null-Unwissenheit und Null-Bewusstsein betrachtet. Wir haben beide Artikel unter einer Überschrift übersetzt und schlagen vor, sie zu diskutieren.

Die ehrgeizigste neue Funktion in C # 8.0 heißt nullbare Referenzen .

Der Zweck dieser neuen Funktion ist es, den Schaden durch eine gefährliche Sache auszugleichen, die der Informatiker Tony Hoar einst als " Milliarden-Dollar-Fehler " bezeichnete. C # hat ein Schlüsselwortnull(das Äquivalent davon findet sich in vielen anderen Sprachen), und die Wurzeln dieses Schlüsselworts lassen sich auf die Sprache Algol W zurückführen, an deren Entwicklung Hoar beteiligt war. In dieser alten Sprache (sie erschien 1966) könnten Variablen, die sich auf Instanzen eines bestimmten Typs beziehen, eine besondere Bedeutung erhalten, die darauf hinweist, dass diese Variable derzeit nirgends referenziert wird. Diese Gelegenheit wurde sehr häufig genutzt, und heute glauben viele Experten (einschließlich Hoar selbst), dass sie zur größten Quelle kostspieliger Softwarefehler aller Zeiten geworden ist.

Was ist falsch daran, Null anzunehmen? In einer Welt, in der ein Link auf Null zeigen kann, müssen Sie dies überall dort berücksichtigen, wo Links in Ihrem Code verwendet werden. Andernfalls besteht die Gefahr, dass Sie zur Laufzeit abgelehnt werden. Manchmal ist es nicht zu lästig. Wenn Sie eine Variable mit einem Ausdruck newan derselben Stelle initialisieren, an der Sie sie deklarieren, wissen Sie, dass diese Variable ungleich Null ist. Aber selbst ein so einfaches Beispiel ist mit einer gewissen kognitiven Belastung behaftet: Vor der Veröffentlichung von C # 8 konnte der Compiler Ihnen nicht sagen, ob Sie etwas tun, in das dieser Wert konvertiert werden kann null. Sobald Sie jedoch verschiedene Codefragmente zusammenfügen, wird es viel schwieriger, solche Dinge mit Sicherheit zu beurteilen: Wie wahrscheinlich ist es, dass diese Eigenschaft, die ich gerade lese, zurückkehren kann null? Darf es senden?nullin diese Methode? In welchen Situationen kann ich sicher sein, dass die von mir aufgerufene Methode dieses Argument outnicht auf null, sondern auf einen anderen Wert setzt? Darüber hinaus beschränkt sich die Angelegenheit nicht einmal darauf, sich daran zu erinnern, solche Dinge zu überprüfen; Es ist nicht ganz klar, was Sie tun sollen, wenn Sie auf Null stoßen.

Bei numerischen Typen in C # gibt es kein solches Problem: Wenn Sie eine Funktion schreiben, die einige Zahlen als Eingabe verwendet und als Ergebnis eine Zahl zurückgibt, müssen Sie sich nicht fragen, ob die übertragenen Werte wirklich Zahlen sind und ob irgendetwas unter ihnen verwechselt werden kann. Wenn Sie eine solche Funktion aufrufen, müssen Sie nicht darüber nachdenken, ob sie anstelle einer Zahl etwas zurückgeben kann. Es sei denn, eine solche Entwicklung von Ereignissen interessiert Sie als Option: In diesem Fall können Sie Parameter oder Ergebnisse des Typs deklarierenint?Dies zeigt an, dass Sie in diesem speziellen Fall wirklich die Übertragung oder Rückgabe eines Nullwerts zulassen möchten. Für numerische Typen und allgemeiner für signifikante Typen war die Nulltoleranz immer eines der Dinge, die als Option freiwillig durchgeführt werden.

Bei den Referenztypen wurde vor C # 8.0 die Zulässigkeit von Null nicht nur standardmäßig festgelegt, sondern konnte auch nicht deaktiviert werden.

Aus Gründen der Abwärtskompatibilität wird die Gültigkeit von Null auch in C # 8.0 standardmäßig fortgesetzt, da neue Sprachfunktionen in diesem Bereich deaktiviert bleiben, bis Sie sie explizit anfordern.

Sobald Sie diese neue Funktion aktivieren, ändert sich jedoch alles. Der einfachste Weg, es zu aktivieren, besteht darin, es <Nullablegt;enable</Nullablegt;innerhalb des Elements hinzuzufügen .<PropertyGroup>in Ihrer Datei .csproj. (Ich stelle fest, dass auch mehr filigrane Steuerung verfügbar ist. Wenn Sie sie wirklich wirklich benötigen, können Sie das Verhalten so konfigurieren, dass es nullin jeder Zeile separat zulässig ist. Als wir uns kürzlich entschieden haben, diese Funktion in alle unsere Projekte aufzunehmen, stellte sich heraus, dass sie in einem Maßstab aktiviert wird Ein Projekt zu einem Zeitpunkt ist eine machbare Aufgabe.)

Wenn die zulässigen Links in C # 8.0 nullvollständig aktiviert sind, ändert sich die Situation: Standardmäßig wird jetzt angenommen, dass die Links nur dann keine Null zulassen, wenn Sie selbst nicht das Gegenteil angeben, genau wie bei signifikanten Typen ( Sogar die Syntax ist dieselbe: Sie könnten int? schreiben, wenn Sie wirklich wollten, dass der ganzzahlige Wert optional ist. Jetzt schreiben Sie Zeichenfolge?, wenn Sie meinen, dass Sie entweder eine Zeichenfolgenreferenz oder möchtennull.)

Dies ist eine sehr bedeutende Änderung, und vor allem ist diese neue Funktion aufgrund ihrer Bedeutung standardmäßig deaktiviert. Microsoft hätte diese Sprachfunktion anders gestalten können: Sie könnten die Standardlinks auf Null setzen und eine neue Syntax einführen, mit der Sie angeben können, dass Sie sicherstellen möchten, dass dies nicht zulässig ist null. Vielleicht würde dies die Messlatte senken, wenn diese Möglichkeit untersucht wird, aber auf lange Sicht wäre eine solche Lösung falsch, da in der Praxis die meisten Links in der riesigen Masse an C # -Code nicht darauf ausgelegt sind, darauf hinzuweisen null.

Angenommen, Null ist eine Ausnahme, keine Regel. Wenn diese neue Sprachfunktion aktiviert ist, wird das Verhindern von Null zu einem neuen Standard. Dies spiegelt sich auch im ursprünglichen Funktionsnamen wider: "nullfähige Referenzen". Der Name ist merkwürdig, da Links nullauf C # 1.0 verweisen könnten . Die Entwickler haben jedoch betont, dass die Nullannahme nun in die Kategorie der Dinge fällt, die explizit angefordert werden müssen.

C # 8.0 vereinfacht das Einführen zulässiger Links null, da Sie diese Funktion schrittweise einführen können. Man muss keine Ja- oder Nein-Wahl treffen. Dies unterscheidet sich erheblich von der async/awaitin C # 5.0 hinzugefügten Funktion , die sich tendenziell ausbreitet: Tatsächlich verpflichten asynchrone Vorgänge den Anrufer dazuasyncDaher muss sich der Code, der diesen Aufrufer aufruft async, usw. ganz oben im Stapel befinden. Glücklicherweise sind Typen, die dies zulassen, nullunterschiedlich aufgebaut: Sie können selektiv und schrittweise implementiert werden. Sie können die Dateien einzeln oder bei Bedarf sogar zeilenweise durcharbeiten.

Der wichtigste Aspekt der zulässigen Typennull(dank dessen der Übergang zu ihnen vereinfacht wird), ist, dass sie standardmäßig deaktiviert sind. Andernfalls würden die meisten Entwickler die Verwendung von C # 8.0 ablehnen, da ein solcher Übergang in fast jeder Codebasis Warnungen verursachen würde. Aus den gleichen Gründen ist der Einstiegsschwellenwert für die Verwendung dieser neuen Funktion jedoch ziemlich hoch: Wenn eine neue Funktion so dramatische Änderungen vornimmt, dass sie standardmäßig deaktiviert ist, möchten Sie sich wahrscheinlich nicht damit anlegen, aber es gibt Probleme beim Umschalten wird immer unnötig Ärger erscheinen. Aber das wäre eine Schande, denn die Funktion ist sehr wertvoll. Es ist hilfreich, Fehler im Code zu finden, bevor Benutzer dies für Sie tun.

Wenn Sie also überlegen, Typen einzuführen, die dies zulassennullBeachten Sie unbedingt, dass Sie diese Funktion Schritt für Schritt einführen können.

Nur Warnungen

Die gröbste Kontrolle über das gesamte Projekt nach einem einfachen Ein- / Ausschalten ist die Möglichkeit, Warnungen unabhängig von Anmerkungen zu aktivieren. Wenn ich beispielsweise die Nullannahme für Corvus.ContentHandling.Json in unserem Corvus.ContentHandling- Repository vollständig aktiviere und <Nullablegt;enable</Nullablegt;der Gruppe von Eigenschaften in der Projektdatei hinzufüge , werden in ihrem aktuellen Status sofort 20 Warnungen vom Compiler angezeigt . Wenn ich es stattdessen verwende, <Nullablegt;warnings</Nullablegt;erhalte ich nur eine Warnung.

Aber warte! Warum werden mir weniger Warnungen angezeigt? Am Ende habe ich hier nur um Warnungen gebeten. Die Antwort auf diese Frage ist nicht ganz offensichtlich: Tatsache ist, dass einige Variablen und Ausdrücke nullnull-ahnungslos sein können.

Nullneutralität

C # unterstützt zwei Interpretationen der Nullgültigkeit. Erstens kann jede Variable eines Referenztyps als zulassend oder nicht zulassend deklariert werden null, und zweitens wird der Compiler nach Möglichkeit logisch schließen, ob sich diese Variable nullan einem bestimmten Punkt im Code befinden kann oder nicht . Dieser Artikel befasst sich nur mit der ersten Art der Zulässigkeitnull, dh über den statischen Typ einer Variablen (tatsächlich gilt dies nicht nur für Variablen und Parameter und Felder, die ihnen im Geiste nahe stehen; sowohl statische als auch logisch ableitbare Zulässigkeit werden nullfür jeden Ausdruck in C # bestimmt.) Tatsächlich ist Zulässigkeit nullin ihrem ersten Sinne Das, was wir in Betracht ziehen, ist eine Erweiterung des Typsystems.

Es stellt sich jedoch heraus, dass die Situation nicht so kohärent ist, wie man annehmen könnte, wenn wir uns nur auf die Nullzulässigkeit für einen Typ konzentrieren. Dies ist nicht nur ein Kontrast zwischen "null Gültigkeit" und "ungültig"null". In der Tat gibt es zwei weitere Möglichkeiten. Es gibt eine Kategorie von „unbekannt“, die aufgrund der Verfügbarkeit von Generika obligatorisch ist. Wenn Sie einen unbegrenzten Typparameter haben, können Sie nichts über die Gültigkeit herausfinden null: Code, der die entsprechende verallgemeinerte Methode oder den Typ verwendet, kann ein Argument in ihnen ersetzen, das entweder zulässt oder nicht zulässt null. Sie können Einschränkungen hinzufügen, aber häufig sind solche Einschränkungen unerwünscht, da sie den Umfang des verallgemeinerten Typs oder der verallgemeinerten Methode einschränken. Für Variablen oder Ausdrücke eines unbegrenzten Typparameters muss die T(Nicht-) Gültigkeit von Null unbekannt sein. vielleicht in jedem Fall die Frage der ZulässigkeitnullEs wird separat für sie entschieden, aber wir wissen nicht, welche Option im generischen Code angezeigt wird, da dies vom Typargument abhängt.

Die letztere Kategorie wird als "neutral" bezeichnet. Nach dem Prinzip der "Neutralität" hat alles vor dem Aufkommen von C # 8.0 funktioniert, und dies funktioniert, wenn Sie die Fähigkeit zum Arbeiten mit nullbaren Links nicht aktivieren. (Grundsätzlich ist dies ein Beispiel für Rückwirkung . Obwohl die Idee der Nullneutralität erstmals in C # 8.0 als natürlicher Codezustand eingeführt wurde, bevor die Nullgültigkeit für Referenzen aktiviert wurde, bestanden C # -Designer darauf, dass diese Eigenschaft C # nie wirklich fremd war.)

Vielleicht müssen Sie nicht erklären, was "Neutralität" in diesem Fall bedeutet, da C # in diesem Sinne immer funktioniert hat, sodass Sie selbst alles verstehen ... obwohl dies vielleicht etwas unehrlich ist. Hören Sie also zu: In einer Welt, in der es um Zulässigkeit geht null, ist das wichtigste Merkmal von nullneutralen Ausdrücken, dass sie keine Warnungen vor Nullakzeptanz auslösen. Sie können einen nullneutralen Ausdruck als zulässige null, aber nicht zulässige Variable zuweisen . Null-neutrale Variablen (sowie Eigenschaften, Felder usw.) können Sie Ausdrücke zuweisen, die der Compiler als "möglich null" oder "nicht null" angesehen hat.

Wenn Sie nur Warnungen aktivieren, gibt es deshalb nicht so viele neue Warnungen. Der gesamte Code verbleibt im Kontext deaktivierter Gültigkeitsanmerkungen null, sodass alle Variablen, Parameter, Felder und Eigenschaften nullneutral sind. Dies bedeutet, dass Sie keine Warnungen erhalten, wenn Sie versuchen, sie in Verbindung mit Entitäten zu verwenden, die dies berücksichtigen null.

Warum bekomme ich dann überhaupt Warnungen? Ein häufiger Grund ist der Versuch, auf inakzeptable Weise Freunde zu finden, die zwei Codeteile berücksichtigen null. Angenommen, ich habe eine Bibliothek, in der zulässige Links vollständig enthalten sind null, und diese Bibliothek verfügt über die folgende tief erfundene Klasse:

public static class NullableAwareClass
	{
	    public static string? GetNullable() => DateTime.Now.Hour > 12 ? null : "morning";
	

	    public static int RequireNonNull(string s) => s.Length;
	}

Außerdem kann ich in einem anderen Projekt diesen Code in dem Kontext schreiben, in dem Nullgültigkeitswarnungen aktiviert sind, die entsprechenden Anmerkungen jedoch deaktiviert sind:

static int UseString(string x) => NullableAwareClass.RequireNonNull(x);

Da Anmerkungen zur Nullgültigkeit deaktiviert sind, ist der Parameter xhier nullneutral. Dies bedeutet, dass der Compiler nicht feststellen kann, ob dieser Code wahr ist oder nicht. Wenn der Compiler Warnungen in Fällen ausgibt, in denen nullneutrale Ausdrücke mit denen gemischt werden, die dies berücksichtigen null, kann ein erheblicher Teil dieser Warnungen als zweifelhaft angesehen werden. Daher werden keine Warnungen ausgegeben.

Mit diesem Wrapper habe ich tatsächlich die Tatsache versteckt, dass der Code die Gültigkeit berücksichtigt null. Das heißt, jetzt kann ich so schreiben:

	int x = UseString(NullableAwareClass.GetNullable());

Der Compiler weiß, was er zurückgeben GetNullablekann null, aber da ich die Methode mit einem nullneutralen Parameter aufgerufen habe, weiß das Programm nicht, ob dies richtig oder falsch ist. Mit dem null-neutralen Wrapper habe ich den Compiler deaktiviert, der hier jetzt kein Problem mehr sieht. Wenn ich diese beiden Methoden jedoch direkt kombinieren würde, wäre alles anders:

int y = NullableAwareClass.RequireNonNull(NullableAwareClass.GetNullable());

Hier übergebe ich das Ergebnis GetNullabledirekt an RequireNonNull. Wenn ich dies in einem Kontext versuchen würde, in dem Nullannahmen aktiviert sind, würde der Compiler eine Warnung generieren, unabhängig davon, ob ich den Kontext der entsprechenden Anmerkungen aktiviert oder deaktiviert habe. In diesem speziellen Fall spielt der Kontext von Anmerkungen keine Rolle, da es keine Deklarationen mit einem Referenztyp gibt. Wenn Sie Warnungen bezüglich der Annahme von Null aktivieren, aber die entsprechenden Anmerkungen deaktivieren, werden alle Deklarationen nullneutral, was jedoch nicht bedeutet, dass alle Ausdrücke solche werden. Wir wissen also, dass das Ergebnis GetNullablenull ist. Daher erhalten wir eine Warnung.

Zusammenfassend: da alle Deklarationen im Kontext von deaktivierten Annotationen, die dies zulassen, nullsindnull-neutral, wir werden nicht viele Warnungen erhalten, da die meisten Ausdrücke null-neutral sind. Der Compiler kann jedoch weiterhin Fehler im Zusammenhang mit der Annahme nullin den Fällen abfangen, in denen die Ausdrücke keinen nullneutralen Intermediär durchlaufen. Darüber hinaus besteht der größte Vorteil in diesem Fall darin, Fehler zu erkennen, die mit Versuchen verbunden sind, potenzielle Nullwerte zu dereferenzieren, indem .beispielsweise verwendet wird:

int z = NullableAwareClass.GetNullable().Length;

Wenn Ihr Code gut gestaltet ist, sollte es keine große Anzahl solcher Fehler geben.

Schrittweise Annotation des gesamten Projekts

Nachdem Sie den ersten Schritt ausgeführt haben, aktivieren Sie einfach die Warnungen und fahren Sie mit der schrittweisen Aktivierung der Annotationen Datei für Datei fort. Es ist praktisch, sie sofort in das gesamte Projekt aufzunehmen, zu sehen, in welchen Dateien Warnungen angezeigt werden, und dann eine Datei auszuwählen, in der relativ wenige Warnungen vorhanden sind. Deaktivieren Sie sie erneut auf der Ebene des gesamten Projekts und schreiben Sie oben in die ausgewählte Datei #nullable enable. Dadurch wird die Annahme null(sowohl für Warnungen als auch für Anmerkungen) in der gesamten Datei vollständig aktiviert (es sei denn, Sie deaktivieren sie mithilfe einer anderen Anweisung erneut#nullable) Anschließend können Sie die gesamte Datei durchgehen und sicherstellen, dass alle Entitäten, bei denen es wahrscheinlich ist, dass sie null sind, als zulässig null(d. H. Hinzufügen ?) gekennzeichnet werden. Anschließend können Sie Warnungen in dieser Datei bearbeiten, falls noch vorhanden.

Es kann sich herausstellen, dass das Hinzufügen aller erforderlichen Anmerkungen alles ist, was erforderlich ist, um alle Warnungen zu beseitigen. Das Gegenteil ist auch möglich: Sie können feststellen, dass Sie eine Datei über die Gültigkeit ordentlich kommentierennull, andere Warnungen sind in anderen Dateien aufgetaucht, die es verwenden. In der Regel gibt es nicht viele solcher Warnungen, und Sie haben Zeit, diese schnell zu beheben. Aber wenn Sie nach diesem Schritt aus irgendeinem Grund nur in Warnungen ertrinken, haben Sie immer noch ein paar Lösungen. Erstens können Sie einfach die Auswahl abbrechen, diese Datei verlassen und eine andere übernehmen. Zweitens können Sie Anmerkungen für die Mitglieder, von denen Sie glauben, dass sie die meisten Probleme verursachen, selektiv deaktivieren. ( #nullableSie können die Direktive so oft verwenden, wie Sie möchten, sodass Sie die Einstellungen für die Nullgültigkeit sogar zeilenweise steuern können, wenn Sie möchten.) Wenn Sie später zu dieser Datei zurückkehren, wenn Sie die Nullgültigkeit in den meisten Projekten bereits aktivieren, sehen Sie möglicherweise weniger Warnungen als beim ersten Mal.

Es gibt Zeiten, in denen Probleme nicht so einfach gelöst werden können. In bestimmten Szenarien im Zusammenhang mit der Serialisierung (z. B. bei Verwendung von Json.NET oder Entity Framework) kann die Arbeit daher schwieriger sein. Ich denke, dieses Problem verdient einen separaten Artikel.

Verknüpfungen mit der Annahme nullverbessern die Ausdruckskraft Ihres Codes und erhöhen die Wahrscheinlichkeit, dass der Compiler Ihre Fehler abfängt, bevor Benutzer auf sie stoßen. Daher ist es besser, diese Funktion nach Möglichkeit einzuschließen. Und wenn Sie es selektiv einbeziehen, werden sich die Vorteile schneller anfühlen.

All Articles