Ich kann eine vernünftige Vermutung anstellen , was hier vor sich geht, aber es ist alles etwas kompliziert :) Es handelt sich um den im Entwurfsspezifikation beschriebenen Nullzustand und die Nullverfolgung . Grundsätzlich warnt der Compiler an dem Punkt, an dem wir zurückkehren möchten, wenn der Status des Ausdrucks "vielleicht null" anstelle von "nicht null" ist.
Diese Antwort ist eher narrativ als nur "hier sind die Schlussfolgerungen" ... Ich hoffe, es ist auf diese Weise nützlicher.
Ich werde das Beispiel etwas vereinfachen, indem ich die Felder entferne, und eine Methode mit einer dieser beiden Signaturen in Betracht ziehen:
public static string M(string? text)
public static string M(string text)
In den folgenden Implementierungen habe ich jeder Methode eine andere Nummer gegeben, damit ich eindeutig auf bestimmte Beispiele verweisen kann. Außerdem können alle Implementierungen im selben Programm vorhanden sein.
In jedem der unten beschriebenen Fälle werden wir verschiedene Dinge tun, aber am Ende versuchen, zurückzukehren text
- es ist also der Nullzustand text
, der wichtig ist.
Bedingungslose Rückgabe
Versuchen wir zunächst, es direkt zurückzugeben:
public static string M1(string? text) => text; // Warning
public static string M2(string text) => text; // No warning
So weit, so einfach. Der nullfähige Status des Parameters zu Beginn der Methode ist "möglicherweise null", wenn er vom Typ iststring?
und "nicht null", wenn er vom Typ ist string
.
Einfache bedingte Rückgabe
Lassen Sie uns nun in der if
Anweisungsbedingung selbst nach Null suchen. (Ich würde den bedingten Operator verwenden, von dem ich glaube, dass er den gleichen Effekt hat, aber ich wollte der Frage treu bleiben.)
public static string M3(string? text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
public static string M4(string text)
{
if (text is null)
{
return "";
}
else
{
return text; // No warning
}
}
Großartig, also sieht es so aus, als ob innerhalb einer if
Anweisung, in der die Bedingung selbst auf Nichtigkeit prüft, der Status der Variablen in jedem Zweig der if
Anweisung unterschiedlich sein kann: innerhalb derelse
Blocks ist der Status in beiden Codeteilen "nicht null". Insbesondere in M3 ändert sich der Zustand von "vielleicht null" zu "nicht null".
Bedingte Rückgabe mit einer lokalen Variablen
Versuchen wir nun, diese Bedingung auf eine lokale Variable zu heben:
public static string M5(string? text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
public static string M6(string text)
{
bool isNull = text is null;
if (isNull)
{
return "";
}
else
{
return text; // Warning
}
}
Sowohl M5 als auch M6 geben Warnungen aus. Wir erhalten also nicht nur nicht den positiven Effekt der Zustandsänderung von "vielleicht null" zu "nicht null" in M5 (wie wir es in M3 getan haben) ... wir bekommen die gegenteiligen Effekt in M6, wo der Zustand von " nicht null "bis" vielleicht null ". Das hat mich wirklich überrascht.
Es sieht also so aus, als hätten wir Folgendes gelernt:
- Die Logik "wie eine lokale Variable berechnet wurde" wird nicht zur Weitergabe von Statusinformationen verwendet. Dazu später mehr.
- Das Einführen eines Nullvergleichs kann den Compiler warnen, dass etwas, von dem er zuvor dachte, dass es nicht null ist, doch null sein könnte.
Bedingungslose Rückgabe nach einem ignorierten Vergleich
Schauen wir uns den zweiten dieser Punkte an, indem wir einen Vergleich vor einer bedingungslosen Rückkehr einführen. (Wir ignorieren also das Ergebnis des Vergleichs vollständig.):
public static string M7(string? text)
{
bool ignored = text is null;
return text; // Warning
}
public static string M8(string text)
{
bool ignored = text is null;
return text; // Warning
}
Beachten Sie, wie sich M8 anfühlt, als sollte es M2 entsprechen - beide haben einen Nicht-Null-Parameter, den sie bedingungslos zurückgeben -, aber die Einführung eines Vergleichs mit Null ändert den Status von "nicht null" in "vielleicht null". Wir können weitere Beweise dafür erhalten, indem wir versuchen, text
vor der Bedingung zu dereferenzieren :
public static string M9(string text)
{
int length1 = text.Length; // No warning
bool ignored = text is null;
int length2 = text.Length; // Warning
return text; // No warning
}
Beachten Sie, dass die return
Anweisung jetzt keine Warnung enthält: Der Status nach der Ausführung text.Length
ist "nicht null" (denn wenn wir diesen Ausdruck erfolgreich ausführen, kann er nicht null sein). Der text
Parameter beginnt also aufgrund seines Typs als "nicht null", wird aufgrund des Nullvergleichs zu "vielleicht null" und wird danach wieder zu "nicht null" text2.Length
.
Welche Vergleiche wirken sich auf den Zustand aus?
Das ist also ein Vergleich von text is null
... welchen Effekt haben ähnliche Vergleiche? Hier sind vier weitere Methoden, die alle mit einem nicht nullbaren String-Parameter beginnen:
public static string M10(string text)
{
bool ignored = text == null;
return text; // Warning
}
public static string M11(string text)
{
bool ignored = text is object;
return text; // No warning
}
public static string M12(string text)
{
bool ignored = text is { };
return text; // No warning
}
public static string M13(string text)
{
bool ignored = text != null;
return text; // Warning
}
Obwohl dies x is object
jetzt eine empfohlene Alternative zu ist x != null
, haben sie nicht den gleichen Effekt: nur einen Vergleich mit null (mit einem von is
, ==
oder!=
) ändert den Zustand von „nicht null“ bis „vielleicht null“.
Warum wirkt sich das Heben des Zustands aus?
Wenn wir zu unserem ersten Aufzählungspunkt zurückkehren, warum berücksichtigen M5 und M6 nicht die Bedingung, die zur lokalen Variablen geführt hat? Das überrascht mich nicht so sehr, wie es andere zu überraschen scheint. Das Einbauen dieser Art von Logik in den Compiler und die Spezifikation ist viel Arbeit und für relativ wenig Nutzen. Hier ist ein weiteres Beispiel, das nichts mit Nullfähigkeit zu tun hat, bei der das Inlining etwas bewirkt:
public static int X1()
{
if (true)
{
return 1;
}
}
public static int X2()
{
bool alwaysTrue = true;
if (alwaysTrue)
{
return 1;
}
// Error: not all code paths return a value
}
Obwohl wir wissen, dass dies alwaysTrue
immer der Fall sein wird, erfüllt es nicht die Anforderungen in der Spezifikation, die den Code nach der if
Anweisung unerreichbar machen, was wir brauchen.
Hier ist ein weiteres Beispiel für eine bestimmte Zuordnung:
public static void X3()
{
string x;
bool condition = DateTime.UtcNow.Year == 2020;
if (condition)
{
x = "It's 2020.";
}
if (!condition)
{
x = "It's not 2020.";
}
// Error: x is not definitely assigned
Console.WriteLine(x);
}
Obwohl wir wissen, dass der Code genau einen dieser if
Anweisungskörper eingibt, gibt es in der Spezifikation nichts, was das klären könnte. Statische Analysewerkzeuge sind möglicherweise in der Lage, dies zu tun, aber es wäre eine schlechte Idee, dies in die Sprachspezifikation aufzunehmen, IMO - es ist in Ordnung, wenn statische Analysewerkzeuge alle Arten von Heuristiken haben, die sich im Laufe der Zeit entwickeln können, aber nicht so sehr für eine Sprachspezifikation.