Betrachten Sie den folgenden Code:
using System;
#nullable enable
namespace Demo
{
public sealed class TestClass
{
public string Test()
{
bool isNull = _test == null;
if (isNull)
return "";
else
return _test; // !!!
}
readonly string _test = "";
}
}
Wenn ich dies erstelle, gibt die mit gekennzeichnete Zeile !!!
eine Compiler-Warnung aus : warning CS8603: Possible null reference return.
.
Ich finde das etwas verwirrend, da _test
es schreibgeschützt und auf ungleich Null initialisiert ist.
Wenn ich den Code wie folgt ändere, verschwindet die Warnung:
public string Test()
{
// bool isNull = _test == null;
if (_test == null)
return "";
else
return _test;
}
Kann jemand dieses Verhalten erklären?
c#
nullable-reference-types
Matthew Watson
quelle
quelle
The Debug.Assert is irrelevant because that is a runtime check
- Dies ist relevant, da die Warnung verschwindet, wenn Sie diese Zeile kommentieren.Debug.Assert
eine Ausnahme ausgelöst wird, wenn der Test fehlschlägt.Debug.Assert
jetzt eine Anmerkung ( src )DoesNotReturnIf(false)
für den Bedingungsparameter.Antworten:
Die Analyse des nullbaren Flusses verfolgt den Nullzustand von Variablen, verfolgt jedoch keinen anderen Zustand, wie z. B. den Wert einer
bool
Variablen (wieisNull
oben), und verfolgt nicht die Beziehung zwischen dem Zustand separater Variablen (z . B.isNull
und_test
).Eine tatsächliche statische Analyse-Engine würde diese Dinge wahrscheinlich tun, wäre aber bis zu einem gewissen Grad auch "heuristisch" oder "willkürlich": Sie konnten nicht unbedingt die Regeln angeben, denen sie folgte, und diese Regeln könnten sich sogar im Laufe der Zeit ändern.
Das können wir nicht direkt im C # -Compiler tun. Die Regeln für nullbare Warnungen sind ziemlich ausgefeilt (wie Jons Analyse zeigt!), Aber sie sind Regeln und können begründet werden.
Bei der Einführung der Funktion scheint es, als hätten wir größtenteils die richtige Balance gefunden, aber es gibt einige Stellen, die sich als unangenehm herausstellen, und wir werden diese für C # 9.0 erneut prüfen.
quelle
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:
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 Nullzustandtext
, der wichtig ist.Bedingungslose Rückgabe
Versuchen wir zunächst, es direkt zurückzugeben:
So weit, so einfach. Der nullfähige Status des Parameters zu Beginn der Methode ist "möglicherweise null", wenn er vom Typ ist
string?
und "nicht null", wenn er vom Typ iststring
.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.)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 derif
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:
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:
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.):
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 :Beachten Sie, dass die
return
Anweisung jetzt keine Warnung enthält: Der Status nach der Ausführungtext.Length
ist "nicht null" (denn wenn wir diesen Ausdruck erfolgreich ausführen, kann er nicht null sein). Dertext
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:Obwohl dies
x is object
jetzt eine empfohlene Alternative zu istx != null
, haben sie nicht den gleichen Effekt: nur einen Vergleich mit null (mit einem vonis
,==
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:
Obwohl wir wissen, dass dies
alwaysTrue
immer der Fall sein wird, erfüllt es nicht die Anforderungen in der Spezifikation, die den Code nach derif
Anweisung unerreichbar machen, was wir brauchen.Hier ist ein weiteres Beispiel für eine bestimmte Zuordnung:
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.quelle
if (x != null) x.foo(); x.bar();
, haben wir zwei Beweisstücke; Dieif
Aussage ist ein Beweis für den Satz "Der Autor glaubt, dass x vor dem Aufruf von foo null sein könnte" und die folgende Aussage ist ein Beweis für "Der Autor glaubt, dass x vor dem Aufruf von bar nicht null ist", und dieser Widerspruch führt zu dem Schlussfolgerung, dass es einen Fehler gibt. Der Fehler ist entweder der relativ harmlose Fehler einer unnötigen Nullprüfung oder der möglicherweise abstürzende Fehler. Welcher Fehler der wahre Fehler ist, ist nicht klar, aber es ist klar, dass es einen gibt.Sie haben Beweise dafür gefunden, dass der Programmablaufalgorithmus, der diese Warnung erzeugt, relativ unkompliziert ist, wenn es darum geht, die in lokalen Variablen codierten Bedeutungen zu verfolgen.
Ich habe keine spezifischen Kenntnisse über die Implementierung des Flow Checkers, aber nachdem ich in der Vergangenheit an Implementierungen ähnlichen Codes gearbeitet habe, kann ich einige fundierte Vermutungen anstellen. Der Flussprüfer leitet im falsch positiven Fall wahrscheinlich zwei Dinge ab: (1)
_test
könnte null sein, denn wenn dies nicht möglich wäre, hätten Sie den Vergleich überhaupt nicht, und (2)isNull
könnte wahr oder falsch sein - weil Wenn es nicht könnte, würden Sie es nicht in einem habenif
. Aber die Verbindung, diereturn _test;
nur ausgeführt wird, wenn_test
nicht null ist, wird diese Verbindung nicht hergestellt.Dies ist ein überraschend kniffliges Problem, und Sie sollten damit rechnen, dass es eine Weile dauern wird, bis der Compiler die Raffinesse von Tools erreicht hat, an denen Experten seit mehreren Jahren arbeiten. Der Coverity Flow Checker zum Beispiel hätte überhaupt kein Problem damit, zu folgern, dass keine Ihrer beiden Varianten eine Nullrendite hatte, aber der Coverity Flow Checker kostet für Firmenkunden viel Geld.
Außerdem sind die Coverity-Prüfer so konzipiert, dass sie über Nacht auf großen Codebasen ausgeführt werden können . Die Analyse des C # -Compilers muss zwischen den Tastenanschlägen im Editor ausgeführt werden , wodurch sich die Art der eingehenden Analysen, die Sie vernünftigerweise durchführen können, erheblich ändert.
quelle
bool b = x != null
vsbool b = x is { }
(mit gibt Keine der tatsächlich verwendeten Zuordnungen!) zeigt, dass selbst die erkannten Muster für Nullprüfungen fraglich sind. Um die zweifellos harte Arbeit des Teams nicht herabzusetzen, damit diese Arbeit größtenteils so funktioniert, wie es für echte, in Gebrauch befindliche Codebasen sein sollte - es sieht so aus, als wäre die Analyse kapitalpragmatisch.Alle anderen Antworten sind ziemlich genau richtig.
Falls jemand neugierig ist, habe ich versucht, die Logik des Compilers unter https://github.com/dotnet/roslyn/issues/36927#issuecomment-508595947 so explizit wie möglich zu formulieren
Das eine Stück, das nicht erwähnt wird, ist, wie wir entscheiden, ob eine Nullprüfung als "rein" betrachtet werden soll, in dem Sinne, dass wir ernsthaft überlegen sollten, ob Null eine Möglichkeit ist, wenn Sie dies tun. Es gibt viele "zufällige" Nullprüfungen in C #, bei denen Sie als Teil einer anderen Aktion auf Null testen. Daher haben wir beschlossen, die Anzahl der Überprüfungen auf diejenigen zu beschränken, von denen wir sicher waren, dass sie absichtlich durchgeführt wurden. Die Heuristik, die wir uns ausgedacht haben, war "enthält das Wort null", deshalb
x != null
undx is object
führen Sie zu unterschiedlichen Ergebnissen.quelle