Warum erhalten wir eine mögliche Dereferenzierungs-Nullreferenzwarnung, wenn eine Nullreferenz nicht möglich zu sein scheint?


9

Nachdem ich diese Frage auf HNQ gelesen hatte, las ich weiter über nullable Reference Types in C # 8 und machte einige Experimente.

Ich bin mir sehr bewusst, dass 9 von 10 oder noch öfter, wenn jemand sagt "Ich habe einen Compiler-Fehler gefunden!" Dies ist eigentlich beabsichtigt und ihr eigenes Missverständnis. Und da ich mich erst heute mit dieser Funktion befasst habe, habe ich offensichtlich kein sehr gutes Verständnis dafür. Schauen wir uns diesen Code an:

#nullable enable
class Program
{
    static void Main()
    {
        var s = "";
        var b = s == null; // If you comment this line out, the warning on the line below disappears
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

Nachdem ich die Dokumentation gelesen habe, auf die ich oben verlinkt habe, würde ich erwarten, dass die s == nullZeile eine Warnung sausgibt - schließlich ist sie eindeutig nicht nullwertfähig, sodass ein Vergleich mit nullnicht sinnvoll ist.

Stattdessen erhalte ich in der nächsten Zeile eine Warnung , und die Warnung besagt, dass sdies eine Nullreferenz ist, obwohl es für einen Menschen offensichtlich ist, dass dies nicht der Fall ist.

Mehr über, wird die Warnung nicht , wenn wir vergleichen nicht angezeigt szu null.

Ich habe ein bisschen gegoogelt und bin auf ein GitHub-Problem gestoßen , bei dem es sich um etwas ganz anderes handelte, aber dabei hatte ich ein Gespräch mit einem Mitwirkenden, der mehr Einblick in dieses Verhalten gab (z. B. "Nullprüfungen sind oft ein nützlicher Weg." den Compiler anzuweisen, seine vorherige Schlussfolgerung über die Nullfähigkeit einer Variablen zurückzusetzen. " ). Dies ließ mich jedoch mit der Hauptfrage unbeantwortet.

Anstatt ein neues GitHub-Problem zu erstellen und möglicherweise die Zeit der unglaublich beschäftigten Projektmitarbeiter in Anspruch zu nehmen, stelle ich dies der Community zur Verfügung.

Könnten Sie mir bitte erklären, was los ist und warum? Insbesondere , warum auf der keine Warnungen generiert s == nullLinie, und warum haben wir , CS8602wenn es scheint nicht , wie eine nullReferenz hier möglich ist? Wenn die Inferenz der Nullbarkeit nicht kugelsicher ist, wie der verknüpfte GitHub-Thread andeutet, wie kann es dann schief gehen? Was wären einige Beispiele dafür?


Es scheint, dass der Compiler selbst das Verhalten festlegt, dass an diesem Punkt die Variable "s" null sein könnte. Wenn ich jemals Strings oder Objekte verwende, sollte es immer eine Überprüfung geben, bevor Sie eine Funktion aufrufen. "s? .Length" sollte den Trick machen und die Warnung selbst sollte verschwinden.
chg

1
@chg, es sollte keine Notwendigkeit geben, ?da snicht nullbar ist. Es wird nicht nullbar, einfach weil wir dumm genug waren, es damit zu vergleichen null.
Andrew Savinykh

Ich habe eine frühere Frage verfolgt (Entschuldigung, kann sie nicht finden), in der festgestellt wurde, dass der Compiler, wenn Sie eine Überprüfung hinzufügen, dass ein Wert null ist, dies als "Hinweis" betrachtet, dass der Wert möglicherweise null ist, selbst wenn dies der Fall ist ist nachweislich nicht der Fall.
stuartd

@ Stuartd, yep, so scheint es zu sein. Die Frage ist nun: Warum ist das nützlich?
Andrew Savinykh

1
@chg, na ja, das sage ich im Fragentext, nicht wahr?
Andrew Savinykh

Antworten:


7

Dies ist praktisch ein Duplikat der Antwort, die @stuartd verlinkt hat, daher werde ich hier nicht auf sehr tiefe Details eingehen. Die Wurzel der Sache ist jedoch, dass dies weder ein Sprachfehler noch ein Compilerfehler ist, sondern dass das beabsichtigte Verhalten genau so ist, wie es implementiert wurde. Wir verfolgen den Nullzustand einer Variablen. Wenn Sie die Variable zum ersten Mal deklarieren, lautet dieser Status NotNull, da Sie ihn explizit mit einem Wert initialisieren, der nicht null ist. Aber wir verfolgen nicht, woher dieser NotNull kam. Dies ist beispielsweise effektiv äquivalenter Code:

#nullable enable
class Program
{
    static void Main()
    {
        M("");
    }
    static void M(string s)
    {
        var b = s == null;
        var i = s.Length; // warning CS8602: Dereference of a possibly null reference
    }
}

In beiden Fällen testen Sie explizit sauf null. Wir nehmen dies als Eingabe für die Flussanalyse, genau wie Mads in dieser Frage geantwortet hat: https://stackoverflow.com/a/59328672/2672518 . In dieser Antwort erhalten Sie bei der Rücksendung eine Warnung. In diesem Fall lautet die Antwort, dass Sie eine Warnung erhalten, dass Sie eine möglicherweise Nullreferenz dereferenziert haben.

Es wird nicht nullbar, einfach weil wir dumm genug waren, es damit zu vergleichen null.

Ja, das tut es tatsächlich. Zum Compiler. Als Menschen können wir uns diesen Code ansehen und offensichtlich verstehen, dass er keine Nullreferenzausnahme auslösen kann. Die Art und Weise, wie die Analyse des nullbaren Flusses im Compiler implementiert wird, kann dies jedoch nicht. Wir haben einige Verbesserungen an dieser Analyse besprochen, bei denen wir zusätzliche Zustände hinzufügen, basierend darauf, woher der Wert stammt, aber wir haben entschieden, dass dies der Implementierung eine große Komplexität hinzufügt, um nicht viel Gewinn zu erzielen, da die einzigen Stellen, an denen Dies ist nützlich in solchen Fällen, in denen der Benutzer eine Variable mit einem newoder einem konstanten Wert initialisiert und sie dann nulltrotzdem überprüft .


Vielen Dank. Dies befasst sich hauptsächlich mit Ähnlichkeiten mit der anderen Frage, ich möchte Unterschiede mehr ansprechen. Warum s == nullwird beispielsweise keine Warnung ausgegeben?
Andrew Savinykh

Auch ich habe das gerade mit gemerkt #nullable enable; string s = "";s = null;kompiliert und funktioniert (es wird immer noch eine Warnung ausgegeben) Was sind die Vorteile der Implementierung, die es ermöglicht, einer "nicht nullbaren Referenz" im aktivierten Nullanmerkungskontext null zuzuweisen?
Andrew Savinykh

Mads Antwort konzentriert sich auf die Tatsache, dass "[Compiler] die Beziehung zwischen dem Status separater Variablen nicht verfolgt". In diesem Beispiel gibt es keine separaten Variablen. Daher habe ich Probleme, den Rest von Mads Antwort auf diesen Fall anzuwenden.
Andrew Savinykh

Es versteht sich von selbst, aber denken Sie daran, ich bin hier, um nicht zu kritisieren, sondern um zu lernen. Ich habe C # seit dem Erscheinen im Jahr 2001 verwendet. Obwohl ich nicht neu in der Sprache bin, war das Verhalten des Compilers für mich überraschend. Das Ziel dieser Frage ist es, die Gründe zu ermitteln, warum dieses Verhalten für den Menschen nützlich ist.
Andrew Savinykh

Es gibt triftige Gründe, dies zu überprüfen s == null. Möglicherweise befinden Sie sich beispielsweise in einer öffentlichen Methode und möchten eine Parameterüberprüfung durchführen. Oder vielleicht verwenden Sie eine Bibliothek, die falsch kommentiert wurde, und bis sie diesen Fehler behoben haben, müssen Sie sich mit null befassen, wo es nicht deklariert wurde. In beiden Fällen wäre es eine schlechte Erfahrung, wenn wir warnen würden. Zum Zulassen der Zuweisung: Lokale Variablenanmerkungen dienen nur zum Lesen. Sie beeinflussen die Laufzeit überhaupt nicht. Tatsächlich haben wir alle diese Warnungen in einem einzigen Fehlercode zusammengefasst, damit Sie sie deaktivieren können, wenn Sie die Code-Abwanderung reduzieren möchten.
333fred

0

Wenn die Inferenz der Nullbarkeit nicht kugelsicher ist, [..] wie kann es dann schief gehen?

Ich habe die nullbaren Referenzen von C # 8 gerne übernommen, sobald sie verfügbar waren. Da ich die Notation [NotNull] (usw.) von ReSharper verwendet habe, habe ich einige Unterschiede zwischen den beiden festgestellt.

Der C # -Compiler kann getäuscht werden, aber er neigt dazu, vorsichtig zu sein (normalerweise nicht immer).

Als Referenz für zukünftige Besucher sind dies die Szenarien, in denen der Compiler ziemlich verwirrt war (ich gehe davon aus, dass alle diese Fälle beabsichtigt sind):

  • Null verzeihen null . Wird häufig verwendet, um die Dereferenzierungswarnung zu vermeiden, das Objekt jedoch nicht auf Null zu setzen. Es sieht so aus, als wollten Sie Ihren Fuß in zwei Schuhen halten.
    string s = null!; //No warning

  • Oberflächenanalyse . Im Gegensatz zu ReSharper (bei dem Code-Annotationen verwendet werden ) unterstützt der C # -Compiler immer noch nicht alle Attribute, um die nullbaren Referenzen zu verarbeiten.
    void DoSomethingWith(string? s)
    {    
        ThrowIfNull(s);
        var split = s.Split(' '); //Dereference warning
    }

Es erlaubt jedoch, ein Konstrukt zu verwenden, um die Nullbarkeit zu überprüfen, wodurch auch die Warnung beseitigt wird:

    public static void DoSomethingWith(string? s)
    {
        Debug.Assert(s != null, nameof(s) + " != null");
        var split = s.Split(' ');  //No warning
    }

oder (immer noch ziemlich coole) Attribute (finden Sie alle hier ):

    public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
    {
        ...
    }

  • Anfällige Code-Analyse . Das hast du ans Licht gebracht. Der Compiler muss Annahmen treffen, um zu funktionieren, und manchmal scheinen sie kontraintuitiv zu sein (zumindest für Menschen).
    void DoSomethingWith(string s)
    {    
        var b = s == null;
        var i = s.Length; // Dereference warning
    }

  • Probleme mit Generika . Fragte hier und erklärte sehr gut hier (gleiche Artikel wie vor Absatz „Das Problem mit T?“). Generika sind kompliziert, da sie sowohl Referenzen als auch Werte glücklich machen müssen. Der Hauptunterschied besteht darin, dass, während string?es sich nur um eine Zeichenfolge handelt, int?eine wird Nullable<int>und der Compiler gezwungen wird, sie auf wesentlich unterschiedliche Weise zu behandeln. Auch hier wählt der Compiler den sicheren Pfad und zwingt Sie, anzugeben, was Sie erwarten:
    public interface IResult<out T> : IResult
    {
        T? Data { get; } //Warning/Error: A nullable type parameter must be known to be a value type or non-nullable reference type.
    }

Gelöste Einschränkungen geben:

    public interface IResult<out T> : IResult where T : class { T? Data { get; }}
    public interface IResult<T> : IResult where T : struct { T? Data { get; }}

Aber wenn wir keine Einschränkungen verwenden und das '?' Aus Daten können wir weiterhin Nullwerte mit dem Schlüsselwort 'default' einfügen:

    [Pure]
    public static Result<T> Failure(string description, T data = default)
        => new Result<T>(ResultOutcome.Failure, data, description); 
        // data here is definitely null. No warning though.

Der letzte scheint mir schwieriger zu sein, da er das Schreiben von unsicherem Code ermöglicht.

Hoffe das hilft jemandem.


Ich würde vorschlagen, die Dokumente zu nullbaren Attributen zu lesen: docs.microsoft.com/en-us/dotnet/csharp/nullable-attributes . Sie lösen einige Ihrer Probleme, insbesondere im Bereich Oberflächenanalyse und Generika.
333fred

Danke für den Link @ 333fred. Obwohl es Attribute gibt, mit denen Sie spielen können, gibt es kein Attribut, das das von mir gepostete Problem löst (etwas, das mir sagt, dass ThrowIfNull(s);es mir versichert, dass ses nicht null ist). In dem Artikel wird auch erklärt, wie nicht nullfähige Generika behandelt werden, während ich zeigte, wie Sie den Compiler "täuschen" können, indem Sie einen Nullwert haben, aber keine Warnung darüber.
Alvin Sartor

Tatsächlich existiert das Attribut. Ich habe einen Fehler in den Dokumenten gemeldet, um ihn hinzuzufügen. Du suchst DoesNotReturnIf(bool).
333fred

@ 333fred eigentlich suche ich etwas ähnlicheres DoesNotReturnIfNull(nullable).
Alvin Sartor
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.