Nullwerte zulassende Verweistypen

C# 8.0 führt Features ein, die Sie verwenden können, um die Wahrscheinlichkeit zu minimieren, dass Ihr Code bewirkt, dass die Runtime System.NullReferenceException auslöst. Es gibt drei Features, mit denen Sie diese Ausnahmen vermeiden können.

  • Verbesserte statische Flussanalyse, die bestimmt, ob eine Variable null sein kann, bevor sie dereferenziert wird.
  • Attribute, die APIs mit Anmerkungen kommentieren, sodass die Flussanalyse den NULL-Status bestimmt.
  • Variablenanmerkungen, die Entwickler verwenden, um den beabsichtigten NULL-Zustand für eine Variable explizit zu deklarieren.

Im restlichen Teil dieses Artikels wird beschrieben, wie diese drei Featurebereiche funktionieren, um Warnungen zu generieren, wenn Ihr Code möglicherweise einen null-Wert dereferenziert. Das Dereferenzieren einer Variablen bedeutet, mithilfe des .-Operators (Punkt) auf einen ihrer Member zuzugreifen, wie im folgenden Beispiel gezeigt:

string message = "Hello, World!";
int length = message.Length; // dereferencing "message"

Wenn Sie eine Variable dereferenzieren, deren Wert null ist, löst die Laufzeit eine System.NullReferenceException aus.

Analyse des NULL-Status

*Analyse des NULL-Status verfolgt den NULL-Status von Verweisen nach. Diese statische Analyse gibt Warnungen aus, wenn Ihr Code möglicherweise null dereferenziert. Sie können diese Warnungen behandeln, um die Fälle zu minimieren, in denen die Laufzeit eine System.NullReferenceException auslöst. Der Compiler verwendet statische Analyse, um den NULL-Status einer Variablen zu bestimmen. Eine Variable ist entweder not-null oder maybe-null. Der Compiler bestimmt auf zwei Arten, dass eine Variable nicht not-null ist:

  1. Die Variable wurde einem Wert zugewiesen, der bekanntermaßen nicht NULL ist.
  2. Die Variable wurde mit null überprüft und seit dieser Überprüfung nicht mehr geändert.

Jede Variable, die der Compiler nicht als not-null bestimmt hat, wird als maybe-null betrachtet. Die Analyse liefert Warnungen in Situationen, in denen Sie einen null-Wert versehentlich dereferenzieren können. Der Compiler generiert Warnungen basierend auf dem NULL-Status.

  • Wenn eine Variable not-null ist, kann diese Variable sicher dereferenziert werden.
  • Wenn eine Variable maybe-null ist, muss diese Variable überprüft werden, um sicherzustellen, dass sie vor der Deferenzierung nicht null ist.

Betrachten Sie das folgenden Beispiel:

string message = null;

// warning: dereference null.
Console.WriteLine($"The length of the message is {message.Length}");

var originalMessage = message;
message = "Hello, World!";

// No warning. Analysis determined "message" is not null.
Console.WriteLine($"The length of the message is {message.Length}");

// warning!
Console.WriteLine(originalMessage.Length);

Im vorherigen Beispiel bestimmt der Compiler, dass message maybe-null ist, wenn die erste Meldung ausgegeben wird. Für die zweite Meldung wird keine Warnung angezeigt. Die letzte Codezeile generiert eine Warnung, weil originalMessage möglicherweise NULL ist. Das folgende Beispiel zeigt eine praktischere Verwendung, um eine Struktur von Knoten bis zum Stamm zu durchlaufen und jeden Knoten während des Durchlaufs zu verarbeiten:

void FindRoot(Node node, Action<Node> processNode)
{
    for (var current = node; current != null; current = current.Parent)
    {
        processNode(current);
    }
}

Der Code oben generiert keine Warnungen zum Dereferenzieren der Variablen current. Die statische Analyse bestimmt, dass current nie dereferenziert wird, wenn der Wert maybe-null ist. Die Variable current wird anhand von null überprüft, bevor auf current.Parent zugegriffen und current an die Aktion ProcessNode übergeben wird. Die vorherigen Beispiele zeigen, wie der Compiler den NULL-Status für lokale Variablen bestimmt, wenn initialisiert, zugewiesen oder mit null verglichen wird.

Attribute für API-Signaturen

Die NULL-Statusanalyse benötigt Hinweise von Entwicklern, um die Semantik von APIs zu verstehen. Einige APIs bieten NULL-Überprüfungen und sollten den null-state einer Variablen von maybe-null in not-null ändern. Andere APIs geben Ausdrücke zurück, die not-null oder maybe-null sind, je nach dem null-state der Eingabeargumente. Sehen Sie sich beispielsweise den folgenden Code an, der eine Meldung anzeigt:

public void PrintMessage(string message)
{
    if (!string.IsNullOrWhiteSpace(message))
    {
        Console.WriteLine($"{DateTime.Now}: {message}");
    }
}

Basierend auf der Überprüfung würde jeder Entwickler diesen Code als sicher betrachten, und er sollte keine Warnungen generieren. Der Compiler weiß nicht, dass IsNullOrWhiteSpace eine NULL-Überprüfung bereitstellt. Sie wenden Attribute an, um den Compiler darüber zu informieren, dass message not-null ist, wenn IsNullOrWhiteSpace false zurückgibt (und nur dann). Im vorherigen Beispiel enthält die Signatur NotNullWhen, um den NULL-Status von message anzugeben:

public static bool IsNullOrWhiteSpace([NotNullWhen(false)] string message);

Attribute bieten ausführliche Informationen zum NULL-Status von Argumenten, Rückgabewerten und Membern der Objektinstanz, die zum Aufrufen eines Members verwendet werden. Die Details zu den einzelnen Attributen finden Sie im Sprachreferenzartikel zu Nullable-Verweisattributen. Alle .NET-Runtime-APIs wurden in .NET 5 mit Anmerkungen kommentiert. Sie verbessern die statische Analyse, indem Sie Ihre APIs kommentieren, um semantische Informationen zum null-state von Argumenten und Rückgabewerten zu liefern.

Nullable-Variablenanmerkungen

Die null-state-Analyse bietet stabile Analysen für die meisten Variablen. Der Compiler benötigt weitere Informationen von Ihnen für Membervariablen. Der Compiler kann keine Annahmen über die Reihenfolge treffen, in der auf öffentliche Member zugegriffen wird. Auf alle öffentlichen Member könnte in beliebiger Reihenfolge zugegriffen werden. Jeder der zugreifbaren Konstruktoren kann verwendet werden, um das Objekt zu initialisieren. Wenn ein Memberfeld jemals auf null festgelegt werden kann, muss der Compiler davon ausgehen, dass sein null-status zu Beginn jeder Methode maybe-null ist.

Sie verwenden Anmerkungen, die deklarieren können, ob eine Variable ein Nullable-Verweistyp oder ein Nicht-Nullable-Verweistyp ist. Diese Anmerkungen enthalten wichtige Anweisungen zum null-state für Variablen:

  • Ein Verweis darf nicht NULL sein. Der Standardstatus einer keine Nullwerte zulassenden Verweisvariable ist nicht-null. Der Compiler erzwingt Regeln, die sicherstellen, dass das Dereferenzieren dieser Variablen sicher ist, ohne zuerst zu überprüfen, ob sie nicht NULL sind:
    • Die Variable muss mit einem Wert ungleich NULL initialisiert werden.
    • Der Variablen kann nie der Wert null zugewiesen werden. Der Compiler gibt eine Warnung aus, wenn der Code einen maybe-null-Ausdruck einer Variablen zuweist, die nicht NULL sein sollte.
  • Ein Verweis darf NULL sein. Der Standardstatus einer Nullwerte zulassenden Verweisvariable ist maybe-null. Der Compiler erzwingt Regeln, um sicherzustellen, dass Sie ordnungsgemäß auf einen null-Verweis überprüft haben:
    • Die Variable kann nur dereferenziert werden, wenn der Compiler garantieren kann, dass der Wert nicht null ist.
    • Diese Variablen können mit dem Standardwert null initialisiert und in anderem Code dem Wert null zugewiesen werden.
    • Der Compiler gibt keine Warnungen aus, wenn Code einer Variablen, die NULL sein kann, einen maybe-null-Ausdruck zuweist.

Jede Verweisvariable, die nicht null sein soll, weist den null-state not-null auf. Jede Verweisvariable, die anfänglich null sein kann, hat den null-state maybe-null.

Ein Nullable-Verweistyp wird mithilfe der gleichen Syntax wie Nullable-Werttypen aufgeführt: Ein ? wird an den Variablentyp angefügt. Beispielsweise stellt die folgende Variablendeklaration eine Nullable-Zeichenfolgenvariable, name, dar:

string? name;

Bei jeder Variable, bei der ? nicht an den Typnamen angefügt ist, handelt es sich um einen Non-Nullable-Verweistyp. Dies umfasst alle Verweistypvariablen in vorhandenem Code, wenn Sie dieses Feature aktiviert haben. Alle implizit typisierten lokalen Variablen (die mit var deklariert wurden) sind Nullable-Verweistypen. Wie in den vorherigen Abschnitten gezeigt, bestimmt die statische Analyse den null-state lokaler Variablen, um zu ermitteln, ob sie maybe-null sind.

Manchmal müssen Sie eine Warnung überschreiben, wenn Sie wissen, dass eine Variable nicht NULL ist, der Compiler aber bestimmt, dass der null-state maybe-null ist. Sie verwenden den NULL-toleranten Operator ! nach einem Variablennamen, um zu erzwingen, dass der null-state not-null ist. Wenn Sie beispielsweise wissen, dass die Variable name nicht null ist, der Compiler aber eine Warnung ausgibt, können Sie folgenden Code schreiben, um die Analyse des Compilers zu überschreiben:

name!.Length;

Nullable-Verweistypen und Nullable-Werttypen bieten ein ähnliches semantisches Konzept: Eine Variable kann einen Wert oder ein Objekt darstellen, oder diese Variable kann null sein. Nullable-Verweistypen und Nullable-Werttypen werden jedoch unterschiedlich implementiert: Nullable-Werttypen werden mit System.Nullable<T> implementiert, und Nullable-Verweistypen werden durch Attribute implementiert, die vom Compiler gelesen werden. string? und string werden z. B. beide durch den gleichen Typ dargestellt: System.String. int? und int werden jedoch durch System.Nullable<System.Int32> bzw. System.Int32 dargestellt.

Generics

Generics erfordern detaillierte Regeln zur Behandlung von T? für jeden Typparameter T. Die Regeln sind aufgrund des bisherigen Verlaufs und der unterschiedlichen Implementierung für einen Nullwerte zulassende Werttyp und einen Nullwerte zulassende Verweistyp notwendigerweise ausführlich. Nullwerte zulassende Werttypen werden mit der Struktur System.Nullable<T> implementiert. Nullwerte zulassende Verweistypen werden als Typ-Anmerkungen implementiert, die dem Compiler semantische Regeln vorgeben.

In C# 8.0 wurde die Verwendung von T? ohne die Einschränkung, dass T ein struct oder ein class sein muss, nicht kompiliert. Dies ermöglichte es dem Compiler, T? eindeutig zu interpretieren. Diese Einschränkung wurde in C# 9.0 aufgehoben, indem die folgenden Regeln für einen nicht eingeschränkten Typparameter T definiert wurden:

  • Wenn das Typargument für T ein Verweistyp ist, verweist T? auf den entsprechenden Nullwerte zulassenden Verweistyp. Wenn zum Beispiel T ein string ist, dann ist T? ein string?.
  • Wenn das Typargument für T ein Wertetyp ist, verweist T? auf denselben Wertetyp, T. Wenn zum Beispiel T ein int ist, ist auch T? ein int.
  • Wenn das Typargument für T ein löschbarer Verweistyp ist, verweist T? auf denselben löschbaren Verweistyp. Wenn zum Beispiel T ein string? ist, dann ist T? auch ein string?.
  • Wenn das Typargument für T ein löschbarer Werttyp ist, verweist T? auf denselben löschbaren Werttyp. Wenn zum Beispiel T ein int? ist, dann ist T? auch ein int?.

Bei Rückgabewerten ist T? äquivalent zu [MaybeNull]T; für Argumentwerte ist T? äquivalent zu [AllowNull]T. Weitere Informationen finden Sie im Artikel Attribute für die Nullzustandsanalyse in der Sprachreferenz.

Mit Einschränkungen können Sie ein anderes Verhalten festlegen:

  • Die class-Einschränkung bedeutet, dass T ein keine Nullwerte zulassender Verweistyp sein muss (z. B. string). Der Compiler gibt eine Warnung aus, wenn Sie einen Nullwerte zulassenden Verweistyp verwenden, z. B. string? für T.
  • Die class?-Einschränkung bedeutet, dass T ein Verweistyp sein muss, entweder ein keine Nullwerte zulassender (string) oder ein Nullwerte zulassender Verweistyp (z. B. string?). Wenn der Typparameter ein löschbarer Verweistyp ist, z. B. string?, verweist ein Ausdruck von T? auf denselben löschbaren Verweistyp, z. B. string?.
  • Die notnull-Einschränkung bedeutet, dass T ein Non-Nullable-Verweistyp oder ein Non-Nullable-Werttyp sein muss. Wenn Sie einen Nullwerte zulassende Verweistyp oder einen Nullwerte zulassende Werttyp für den Typparameter verwenden, generiert der Compiler eine Warnung. Wenn T ein Werttyp ist, ist der Rückgabewert dieser Werttyp und nicht der entsprechende löschbare Werttyp.

Diese Einschränkungen helfen dem Compiler, weitere Informationen zur Verwendung von T zu erhalten. Dies hilft, wenn Entwickler den Typ für T auswählen, und bietet eine bessere null-state-Analyse, wenn eine Instanz des generischen Typs verwendet wird.

Nullable-Kontexte

Die neuen Features, die vor dem Auslösen von System.NullReferenceException schützen, können störend sein, wenn sie in einer vorhandenen Codebasis aktiviert werden:

  • Alle explizit typisierten Verweisvariablen werden als Non-Nullable-Verweistypen interpretiert.
  • Die Bedeutung der class-Einschränkung in Generika wurde geändert, um einen Non-Nullable-Verweistyp anzugeben.
  • Aufgrund dieser neuen Regeln werden neue Warnungen generiert.

Sie müssen sich ausdrücklich für die Nutzung dieser Funktionen in Ihren Projekten entscheiden. Dies bietet einen Migrationspfad und bewahrt Abwärtskompatibilität. Nullable-Kontexte ermöglichen eine differenzierte Steuerung der Interpretation von Verweistypvariablen durch den Compiler. Der Nullable-Anmerkungskontext bestimmt das Verhalten des Compilers. Es gibt vier Werte für den Nullable-Anmerkungskontext:

  • disabled: Der Compiler verhält sich ähnlich wie C# 7.3 und früher:
    • Nullable-Warnungen sind deaktiviert.
    • Alle Verweistypvariablen sind Nullable-Verweistypen.
    • Sie können eine Variable nicht als Nullable-Verweistyp deklarieren, indem Sie das Suffix ? für den Typ verwenden.
    • Sie können den NULL-toleranten Operator (!) verwenden, das hat aber keine Auswirkungen.
  • enabled: Der Compiler aktiviert alle NULL-Verweisanalysen und alle Sprachfeatures.
    • Alle neuen Nullable-Warnungen sind aktiviert.
    • Sie können das Suffix ? verwenden, um einen Nullable-Verweistyp zu deklarieren.
    • Alle anderen Verweistypvariablen sind Non-Nullable-Verweistypen.
    • Der NULL-tolerante Operator unterdrückt Warnungen für eine mögliche Zuweisung zu null.
  • warnings: Der Compiler führt alle NULL-Analysen durch und gibt Warnungen aus, wenn Code möglicherweise null dereferenziert.
    • Alle neuen Nullable-Warnungen sind aktiviert.
    • Verwenden Sie das Suffix ? verwenden, um einen Nullable-Verweistyp zu deklarieren, der eine Warnung generiert.
    • Alle Verweistypvariablen dürfen NULL sein. Member haben jedoch den null-state not-null an der öffnenden geschweiften Klammer aller Methoden, es sei denn, sie werden mit dem Suffix ? deklariert.
    • Sie können den NULL-toleranten Operator (!) verwenden.
  • annotations: Der Compiler führt keine NULL-Analyse durch oder gibt Warnungen aus, wenn Code möglicherweise null dereferenziert.
    • Alle neuen Nullable-Warnungen sind deaktiviert.
    • Sie können das Suffix ? verwenden, um einen Nullable-Verweistyp zu deklarieren.
    • Alle anderen Verweistypvariablen sind Non-Nullable-Verweistypen.
    • Sie können den NULL-toleranten Operator (!) verwenden, das hat aber keine Auswirkungen.

Verweistypvariablen in Code, der vor C# 8 kompiliert wurde, oder in einem deaktivierten Kontext sind Nullwerte zulassend-nicht beachtend. Sie können ein null-Literal oder eine maybe-null-Variable einer Variablen zuweisen, die Nullwerte zulassend-nicht beachtend ist. Der Standardzustand einer Variable mit der Eigenschaft Nullwerte zulassend-nicht beachtend ist jedoch nicht-null.

Sie können auswählen, welche Einstellung für Ihr Projekt am besten geeignet ist:

  • Wählen Sie disabled für Legacyprojekte aus, die Sie nicht basierend auf Diagnosen oder neuen Features aktualisieren möchten.
  • Wählen Sie warnings aus, um zu bestimmen, wo Ihr Code möglicherweise System.NullReferenceExceptions auslösen kann. Sie können diese Warnungen beheben, bevor Sie den Code ändern, um Non-Nullable-Verweistypen zu aktivieren.
  • Wählen Sie annotations aus, um Ihre Entwurfsabsicht auszudrücken, bevor Sie Warnungen aktivieren.
  • Wählen Sie enabled für neue Projekte und aktive Projekte aus, die Sie vor NULL-Verweisausnahmen schützen möchten.

Der Nullable-Anmerkungskontext und der Nullable-Warnungskontext können für ein Projekt festgelegt werden, indem Sie das <Nullable>-Element in Ihrer CSPROJ-Datei verwenden. Dieses Element konfiguriert, wie der Compiler die NULL-Zulässigkeit von Typen interpretiert und welche Warnungen generiert werden. Gültige Einstellungen sind folgende:

  • enable
  • warnings
  • annotations
  • disable

Beispiel:

<Nullable>enable</Nullable>

Sie können auch Anweisungen verwenden, um diese gleichen Kontexte an beliebiger Stelle in Ihrem Quellcode festzulegen. Diese sind besonders nützlich, wenn Sie eine große Codebasis migrieren.

  • #nullable enable: Legt den Nullable-Anmerkungskontext und den Nullable-Warnungskontext auf enabled (aktiviert) fest.
  • #nullable disable: Legt den Nullable-Anmerkungskontext und den Nullable-Warnungskontext auf disabled (deaktiviert) fest.
  • #nullable restore: Stellt die Projekteinstellungen für den Nullable-Anmerkungskontext und den Nullable-Warnungskontext wieder her.
  • #nullable disable warnings: Legt den Nullable-Warnungskontext auf disabled (deaktiviert) fest.
  • #nullable enable warnings: Legt den Nullable-Warnungskontext auf enabled (aktiviert) fest.
  • #nullable restore warnings: Stellt die Projekteinstellungen für den Nullable-Warnungskontext wieder her.
  • #nullable disable annotations: Legt den Nullable-Anmerkungskontext auf disabled (deaktiviert) fest.
  • #nullable enable annotations: Legt den Nullable-Anmerkungskontext auf enabled (aktiviert) fest.
  • #nullable restore annotations: Stellt die Projekteinstellungen für den Anmerkungswarnungskontext wieder her.

Für jede Codezeile können Sie eine der folgenden Kombinationen festlegen:

Warnungskontext Anmerkungskontext Zweck
Standardeinstellung des Projekts Standardeinstellung des Projekts Standard
enabled deaktiviert Analysewarnungen korrigieren
enabled Standardeinstellung des Projekts Analysewarnungen korrigieren
Standardeinstellung des Projekts enabled Typanmerkungen hinzufügen
enabled enabled Bereits migrierter Code
deaktiviert enabled Kommentieren von Code vor dem Beheben von Warnungen
deaktiviert deaktiviert Hinzufügen von Legacycode zum migrierten Projekt
Standardeinstellung des Projekts deaktiviert Selten
deaktiviert Standardeinstellung des Projekts Selten

Mit diesen neun Kombinationen können Sie die Diagnosen, die der Compiler für Ihren Code ausgibt, detailliert steuern. Sie können weitere Features in jedem Bereich aktivieren, den Sie aktualisieren, ohne zusätzliche Warnungen zu erhalten, die Sie noch nicht beheben möchten.

Wichtig

Der globale Nullable-Kontext gilt nicht für generierte Codedateien. Der Nullable-Kontext ist unabhängig von der Strategie für alle als generiert gekennzeichneten Quelldateien deaktiviert. Das bedeutet, dass alle in generierten Dateien enthaltenen APIs nicht mit Anmerkungen versehen werden. Es gibt viel Möglichkeiten, eine Datei als generiert zu markieren:

  1. Geben Sie in der EDITORCONFIG-Datei generated_code = true in einem Abschnitt an, der für diese Datei gilt.
  2. Fügen Sie <auto-generated> oder <auto-generated/> ganz oben in der Datei in einem Kommentar ein. Dabei kann es sich um eine beliebige Zeile des Kommentars handeln, jedoch muss es sich beim Kommentarblock um das erste Element in der Datei handeln.
  3. Beginnen Sie den Dateinamen mit TemporaryGeneratedFile_ .
  4. Enden Sie den Dateinamen mit .designer.cs, .generated.cs, .g.cs oder .g.i.cs.

Generatoren können die Präprozessoranweisung #nullable verwenden.

Standardmäßig sind die Nullable-Anmerkungs- und -Warnungskontexte deaktiviert. Dies bedeutet, dass Ihr vorhandener Code ohne Änderungen und ohne Warnungen kompiliert wird. Ab .NET 6 enthalten neue Projekte das <Nullable>enable</Nullable>-Element in allen Projektvorlagen.

Diese Optionen bieten zwei unterschiedliche Strategien zum Aktualisieren einer vorhandenen Codebasis, um Nullable-Verweistypen zu verwenden.

Bekannte Fehlerquellen

Arrays und Strukturen, die Verweistypen enthalten, sind bekannte Fallstricke in Nullable-Verweisen und in der statischen Analyse, die die NULL-Sicherheit bestimmt. In beiden Fällen kann ein Non-Nullable-Verweis mit null initialisiert werden, ohne Warnungen zu generieren.

Strukturen

Strukturen, die Verweistypen enthalten, die keine NULL-Werte zulassen, können Sie default zuweisen, ohne dass Warnungen ausgelöst werden. Betrachten Sie das folgenden Beispiel:

using System;

#nullable enable

public struct Student
{
    public string FirstName;
    public string? MiddleName;
    public string LastName;
}

public static class Program
{
    public static void PrintStudent(Student student)
    {
        Console.WriteLine($"First name: {student.FirstName.ToUpper()}");
        Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}");
        Console.WriteLine($"Last name: {student.LastName.ToUpper()}");
    }

    public static void Main() => PrintStudent(default);
}

Im Beispiel oben gibt es in PrintStudent(default) keine Warnung, wenn die Non-Nullable-Verweistypen FirstName und LastName NULL sind.

Bei der Verwendung von generischen Strukturen tritt ein weiteres häufigeres Problem auf. Betrachten Sie das folgenden Beispiel:

#nullable enable

public struct Foo<T>
{
    public T Bar { get; set; }
}

public static class Program
{
    public static void Main()
    {
        string s = default(Foo<string>).Bar;
    }
}

Im obigen Beispiel entspricht die Eigenschaft Bar zur Laufzeit null, und sie wird einer Zeichenfolge zugewiesen, die keine NULL-Werte zulässt, ohne dass eine Warnung ausgelöst wird.

Arrays

Arrays stellen ebenfalls eine bekannte Fehlerquelle in Verweistypen dar, die NULL-Werte zulassen. Sehen Sie sich das folgende Beispiel an, das keine Warnungen auslöst:

using System;

#nullable enable

public static class Program
{
    public static void Main()
    {
        string[] values = new string[10];
        string s = values[0];
        Console.WriteLine(s.ToUpper());
    }
}

Im Beispiel oben zeigt die Deklaration des Arrays, dass dieses Non-Nullable-Zeichenfolgen enthält, während alle seine Elemente mit null initialisiert werden. Anschließend wird der Variablen s ein null-Wert (das erste Element des Arrays) zugewiesen. Schließlich wird die Variable s dereferenziert, was zu einer Laufzeitausnahme führt.

Siehe auch