Warum erhalte ich keine Warnung vor einer möglichen Dereferenzierung einer Null in C # 8 mit einem Klassenmitglied einer Struktur?

8

In einem C # 8-Projekt mit aktivierten nullfähigen Referenztypen habe ich den folgenden Code, der meiner Meinung nach eine Warnung vor einer möglichen Null-Dereferenzierung geben sollte, aber nicht:

public class ExampleClassMember
{
    public int Value { get; }
}

public struct ExampleStruct
{
    public ExampleClassMember Member { get; }
}

public class Program
{
    public static void Main(string[] args)
    {
        var instance = new ExampleStruct();
        Console.WriteLine(instance.Member.Value);  // expected warning here about possible null dereference
    }
}

Wenn instancemit dem Standard - Konstruktor initialisiert wird, instance.Memberwird auf den Standardwert gesetzt ExampleClassMember, das ist null. Somit instance.Member.Valuewird NullReferenceExceptionzur Laufzeit ein geworfen . Da ich die Nullfähigkeitserkennung von C # 8 verstehe, sollte ich eine Compiler-Warnung über diese Möglichkeit erhalten, aber ich weiß nicht; warum ist das so?

DylanSp
quelle
Haben Sie dies als Problem im Roslyn GitHub-Repo eingereicht?
Dai
@Dai habe ich (noch) nicht; Wenn es ein legitimer Fehler ist und nicht etwas, das mir fehlt, werde ich es tun.
DylanSp
FWIW, dieser Code wird in C # 7.0 nicht kompiliert. Ich erhalte eine Fehlermeldung zu den beiden Typen, denen Konstruktoren fehlen, um die Werte der automatischen Eigenschaften festzulegen. Es wird jedoch mit den Roslyn 3.0- und .NET Core 3.0-Compilern kompiliert und in beiden letzteren Fällen mit einem NRE ausgeführt. Ich verwende eine webbasierte IDE, ohne dass Compileroptionen festgelegt werden können.
Dai
Der C # 8.0-Compiler warnt mich, wenn ich ExampleStructvon structzu wechsle class.
Dai
1
@tymtam, das ist für eine Vorschau-Version. In der Release-Version ist esNullable
Panagiotis Kanavos

Antworten:

13

Beachten Sie, dass es keinen Grund für eine Warnung beim Anruf an gibt Console.WriteLine(). Die Referenztyp-Eigenschaft ist kein nullfähiger Typ, sodass der Compiler nicht warnen muss, dass sie möglicherweise null ist.

Sie könnten argumentieren, dass der Compiler vor der Referenz in sich structselbst warnen sollte . Das erscheint mir vernünftig. Aber das tut es nicht. Dies scheint eine Lücke zu sein, die durch die Standardinitialisierung für Werttypen verursacht wird, dh es muss immer einen Standardkonstruktor (ohne Parameter) vorhanden sein, der immer nur alle Felder auf Null setzt (Nullen für Referenztypfelder, Nullen für numerische Typen usw.). ).

Ich nenne es eine Lücke, weil theoretisch nicht nullbare Referenzwerte eigentlich immer nicht null sein sollten! Duh. :) :)

Diese Lücke scheint in diesem Blog-Artikel behoben zu werden: Einführung in nullable Referenztypen in C #

Vermeiden von Nullen Bisher ging es bei den Warnungen darum, Nullen in nullbaren Referenzen vor einer Dereferenzierung zu schützen. Die andere Seite der Medaille besteht darin, zu vermeiden, dass die nicht stornierbaren Referenzen Nullen enthalten.

Es gibt verschiedene Möglichkeiten, wie Nullwerte entstehen können, und die meisten von ihnen sind eine Warnung wert, während einige von ihnen ein weiteres „Meer von Warnungen“ verursachen würden, das besser zu vermeiden ist:

  • Verwenden des Standardkonstruktors einer Struktur mit einem Feld vom nicht löschbaren Referenztyp. Dieser ist hinterhältig, da der Standardkonstruktor (der die Struktur auf Null setzt) ​​an vielen Stellen sogar implizit verwendet werden kann. Wahrscheinlich ist es besser, nicht zu warnen [Hervorhebung meiner - PD] , sonst würden viele vorhandene Strukturtypen unbrauchbar.

Mit anderen Worten, ja, das ist eine Lücke, aber nein, es ist kein Fehler. Die Sprachdesigner sind sich dessen bewusst, haben sich jedoch entschieden, dieses Szenario aus den Warnungen herauszulassen, da dies angesichts der Funktionsweise der structInitialisierung unpraktisch wäre .

Beachten Sie, dass dies auch im Einklang mit der breiteren Philosophie steht, die hinter der Funktion steht. Aus dem gleichen Artikel:

Wir möchten, dass es sich über Ihren vorhandenen Code beschwert. Aber nicht widerlich. So werden wir versuchen, dieses Gleichgewicht herzustellen:

  1. Es gibt keine garantierte Nullsicherheit [Schwerpunkt Mine - PD] , selbst wenn Sie auf alle Warnungen reagieren und diese beseitigen. Es gibt viele Lücken in der Analyse nach Notwendigkeit und auch einige nach Wahl.

Bis zu diesem letzten Punkt: Manchmal ist eine Warnung die „richtige“ Vorgehensweise, wird jedoch ständig auf vorhandenen Code ausgelöst, selbst wenn dieser tatsächlich auf null sichere Weise geschrieben ist. In solchen Fällen irren wir auf der Seite der Bequemlichkeit, nicht der Korrektheit. Wir können kein „Meer von Warnungen“ für vorhandenen Code ausgeben: Zu viele Leute würden die Warnungen einfach wieder ausschalten und niemals davon profitieren.

Beachten Sie auch, dass dasselbe Problem bei Arrays von nominell nicht nullbaren Referenztypen (z string[]. B. ) besteht. Wenn Sie das Array erstellen, sind alle Referenzwerte gültig. nullDies ist jedoch zulässig und generiert keine Warnungen.


Soviel zur Erklärung, warum die Dinge so sind, wie sie sind. Dann stellt sich die Frage, was man dagegen tun soll. Das ist viel subjektiver und ich glaube nicht, dass es eine richtige oder falsche Antwort gibt. Das gesagt…

Ich persönlich würde meine structTypen von Fall zu Fall behandeln. Für diejenigen, bei denen die Absicht tatsächlich ein nullbarer Referenztyp ist, würde ich die ?Anmerkung anwenden . Sonst würde ich nicht.

Technisch gesehen sollte jeder einzelne Referenzwert in a struct"nullbar" sein, dh die ?nullbare Annotation mit dem Typnamen enthalten. Aber wie bei vielen ähnlichen Funktionen (wie async / await in C # oder constin C ++) hat dies einen "ansteckenden" Aspekt, da Sie diese Annotation entweder später überschreiben müssen (mit der !Annotation) oder eine explizite Nullprüfung einschließen müssen oder weisen Sie diesen Wert immer nur einer anderen nullfähigen Referenztypvariablen zu.

Für mich hat dies einen großen Nachteil darin, nullfähige Referenztypen zu aktivieren. Da solche Mitglieder von structTypen ohnehin irgendwann eine Sonderfallbehandlung erfordern und die einzige Möglichkeit, sie wirklich sicher zu behandeln, während sie weiterhin nicht nullfähige Referenztypen verwenden können, darin besteht, überall, wo Sie die verwenden struct, Nullprüfungen durchzuführen , bin ich der Meinung Es ist eine vernünftige Implementierungsentscheidung, zu akzeptieren, dass der Code bei der Initialisierung des structCodes dafür verantwortlich ist, dies korrekt zu tun und sicherzustellen, dass das nicht nullbare Referenztypelement tatsächlich auf einen nicht nullwert initialisiert wird.

Dies kann durch die Bereitstellung eines "offiziellen" Initialisierungsmittels unterstützt werden, z. B. eines nicht standardmäßigen Konstruktors (dh eines mit Parametern) oder einer Factory-Methode. Es besteht immer noch das Risiko, dass der Standardkonstruktor oder überhaupt kein Konstruktor verwendet wird (wie bei Array-Zuweisungen). Durch die Bereitstellung einer bequemen Möglichkeit zur structkorrekten Initialisierung des Codes wird jedoch Code gefördert, der ihn verwendet, um Nullreferenzen in Nicht- zu vermeiden nullfähige Variablen.

Wenn Sie jedoch 100% ige Sicherheit in Bezug auf nullfähige Referenztypen wünschen, besteht der richtige Ansatz für dieses bestimmte Ziel eindeutig darin, jedes Referenztypelement in einem structmit zu kommentieren ?. Dies bedeutet, dass jedes Feld und jede automatisch implementierte Eigenschaft zusammen mit jeder Methode oder jedem Eigenschafts-Getter, der solche Werte oder das Produkt solcher Werte direkt zurückgibt. Dann muss der konsumierende Code an jedem Punkt, an dem solche Werte in nicht nullfähige Variablen kopiert werden, Nullprüfungen oder den Nullverzeihungsoperator enthalten.

Peter Duniho
quelle
Gute Analyse und danke, dass Sie diesen Blog-Beitrag gefunden haben - es ist eine ziemlich schlüssige Antwort.
DylanSp
1

Angesichts der hervorragenden Antwort von @ peter-duniho scheint es ab Oktober 2019 am besten zu sein, alle Mitglieder ohne Werttyp als nullfähige Referenz zu markieren.

#nullable enable
public class C
{
    public int P1 { get; } 
}

public struct S
{
    public C? Member { get; } // Reluctantly mark as nullable reference because
                              // https://devblogs.microsoft.com/dotnet/nullable-reference-types-in-csharp/
                              // states:
                              // "Using the default constructor of a struct that has a
                              // field of nonnullable reference type. This one is 
                              // sneaky, since the default constructor (which zeroes 
                              // out the struct) can even be implicitly used in many
                              // places. Probably better not to warn, or else many
                              // existing struct types would be rendered useless."
}

public class Program
{
    public static void Main()
    {
        var instance = new S();
        Console.WriteLine(instance.Member.P1); // Warning
    }
}
Tymtam
quelle