Start Debugging

Lösung: CS8618 "Non-nullable property must contain a non-null value when exiting constructor" in C#

CS8618 bedeutet, dass ein nicht-nullbares Feld oder eine Eigenschaft beim Verlassen des Konstruktors nicht initialisiert war. Weisen Sie es im Konstruktor zu, geben Sie einen Standardwert, markieren Sie es als required oder machen Sie es nullbar.

CS8618 wird ausgelöst, wenn für ein nicht-nullbares Referenzmitglied (ein Feld oder eine Auto-Eigenschaft) nicht garantiert ist, dass es beim Beenden eines Konstruktors einen Nicht-Null-Wert enthält. Der Compiler kann nicht beweisen, dass das Mitglied zugewiesen wurde, und warnt daher, dass ein null entweichen könnte. Beheben Sie es auf eine von vier Arten, in ungefährer Reihenfolge der Präferenz: weisen Sie es im Konstruktor zu, geben Sie ihm einen Feldinitialisierer, markieren Sie es als required, sodass der Aufrufer es setzen muss, oder machen Sie das Mitglied nullbar (string?), falls null tatsächlich gültig ist. Dies wurde mit C# 14 auf .NET 11 verifiziert; die Diagnose verhält sich so, seit nullbare Referenztypen in C# 8 eingeführt wurden, und .NET 6 war die Version, die den nullbaren Kontext in neuen Projekten standardmäßig aktiviert hat.

Der Fehler im Kontext

Der aktuelle Compiler gibt eine einheitliche Meldung für Felder und Eigenschaften aus:

warning CS8618: Non-nullable variable must contain a non-null value when exiting constructor. Consider declaring it as nullable.

Ältere SDKs (und viele noch offene StackOverflow-Threads) zeigen die feld- und eigenschaftsspezifischen Formulierungen, die viele tatsächlich in die Suche eingeben:

warning CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor.
warning CS8618: Non-nullable field '_name' must contain a non-null value when exiting constructor.

Alle drei sind dieselbe Diagnose mit derselben Ursache. Beachten Sie das Wort warning, nicht error: CS8618 stoppt den Build standardmäßig nicht. Es wird nur dann zu einem den Build brechenden Fehler, wenn Sie <TreatWarningsAsErrors>true</TreatWarningsAsErrors> oder <WarningsAsErrors>CS8618</WarningsAsErrors> in Ihrem Projekt haben, was viele Teams genau deshalb tun, damit Lücken in der Null-Sicherheit nicht ignoriert werden können.

Warum das passiert

Nullbare Referenztypen, in C# 8 eingeführt und seit .NET 6 in den Vorlagen standardmäßig aktiviert (<Nullable>enable</Nullable> in der .csproj), teilen jeden Referenztyp in zwei Zustände auf: nicht-nullbar (string) und nullbar (string?). Ein nicht-nullbares Mitglied ist ein Versprechen: “dies wird niemals null sein.” Die Aufgabe des Compilers ist es, Sie an dieses Versprechen zu halten, und die Stelle, an der er das am einfachsten prüfen kann, ist die Konstruktion. Wenn ein Konstruktor zurückkehrt, muss jedes nicht-nullbare Feld und jede nicht-nullbare Auto-Eigenschaft nachweislich nicht null sein. Kann der Compiler das nicht beweisen, erhalten Sie CS8618.

Der entscheidende Ausdruck ist “nachweislich.” Der Compiler führt eine statische Analyse durch; er führt Ihren Code nicht aus. Er vertraut genau drei Dingen: einem Feld- oder Eigenschaftsinitialisierer, einer direkten Zuweisung innerhalb des Konstruktors und einer Hilfsmethode, die annotiert ist, um zu sagen, dass sie das Mitglied zuweist. Ein Konstruktor, der den Wert über einen Pfad zuweist, dem der Compiler nicht folgen kann, oder ein Mitglied, das ein Framework erst später setzt, zählt gar nicht. Das ist dasselbe “Beweisen, nicht Zeigen”-Modell hinter der Diagnose für erforderliche Mitglieder CS9035: Der Compiler liest die Absicht nicht aus Ihren Methodenkörpern.

Eine subtile Falle: Eine Absicherung mit einer Null-Prüfung innerhalb des Konstruktors hilft nicht. Code wie if (name is null) throw new ArgumentNullException(nameof(name)); beweist, dass der Parameter nicht null ist, aber der Compiler sieht das Mitglied weiterhin als nicht zugewiesen an, solange Sie es nicht tatsächlich zuweisen. Das überrascht Entwickler oft genug, um ein eigenes langlebiges Roslyn-Issue zu haben.

Minimale Reproduktion

Der kleinste Typ, der CS8618 auslöst, in einem Projekt mit aktiviertem nullbarem Kontext:

// .NET 11, C# 14, <Nullable>enable</Nullable>
public class Person
{
    public string Name { get; set; }    // CS8618: never assigned
    public string Email { get; set; }   // CS8618: never assigned
    public int Age { get; set; }        // fine, value type has a default
}

Zwei Warnungen, eine pro nicht-nullbarer Referenzeigenschaft. Age bleibt still, weil Werttypen immer einen Standard haben (0); Nullbarkeitswarnungen betreffen Referenztypen. Fügen Sie einen Konstruktor hinzu, der nur ein Mitglied setzt, und Sie erhalten weiterhin eine Warnung:

// .NET 11, C# 14
public class Person
{
    public Person(string name)
    {
        Name = name;      // Name is proven
    }

    public string Name { get; set; }
    public string Email { get; set; }   // CS8618: still not assigned on this path
}

Der Compiler prüft jeden Konstruktor unabhängig. Lässt irgendein Konstruktor ein nicht-nullbares Mitglied unzugewiesen, erzeugt dieser Konstruktor die Warnung.

Die Lösung im Detail

Arbeiten Sie diese Optionen der Reihe nach durch. Die ersten drei sind die, die Sie meistens wollen; die letzten beiden sind Notausgänge für den Fall, dass das Mitglied tatsächlich an einer Stelle initialisiert wird, die der Compiler nicht sehen kann.

1. Initialisieren Sie das Mitglied in einem Konstruktor

Wird der Wert benötigt, um ein gültiges Objekt zu bauen, nehmen Sie ihn als Konstruktorparameter und weisen Sie ihn zu. Das ist das Design, zu dem die Warnung Sie drängt:

// .NET 11, C# 14
public class Person
{
    public Person(string name, string email)
    {
        Name = name;
        Email = email;
    }

    public string Name { get; set; }
    public string Email { get; set; }
}

Beide Mitglieder sind nun auf jedem Konstruktionspfad nachweislich zugewiesen, sodass beide Warnungen verschwinden. Haben Sie mehrere Konstruktoren, leiten Sie sie durch einen einzigen, damit die Zuweisung an einer einzigen Stelle liegt: public Person() : this("John", "Doe") { } stellt den Compiler zufrieden, weil der verkettete Konstruktor die Arbeit erledigt.

2. Geben Sie dem Mitglied einen Standardwert mit einem Feldinitialisierer

Gibt es einen sinnvollen Standard und wollen Sie nicht jeden Aufrufer zwingen, den Wert zu übergeben, initialisieren Sie das Mitglied dort, wo es deklariert wird:

// .NET 11, C# 14
public class Person
{
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
}

Ein Feldinitialisierer läuft vor jedem Konstruktorkörper, sodass das Mitglied auf jedem Pfad automatisch nicht null ist. Das ist die sauberste Lösung für eher optionale Werte wie leere Zeichenfolgen oder new List<string>()-Sammlungen. Es ist auch besser, als den Typ nullbar zu machen, wenn das Mitglied zur Laufzeit niemals null sein sollte, weil es den Nicht-Null-Vertrag für alle wahrt, die die Eigenschaft lesen.

3. Markieren Sie das Mitglied als required (C# 11 und später)

Ist das Mitglied verpflichtend, Sie wollen dafür aber keinen Konstruktorparameter, verwenden Sie den Modifikator required. Er verschiebt die Verpflichtung in den Objektinitialisierer des Aufrufers und bringt als Bonus CS8618 zum Schweigen, weil der Compiler nun weiß, dass das Mitglied gesetzt werden muss, bevor das Objekt entweicht:

// .NET 11, C# 14
public class Person
{
    public required string Name { get; set; }
    public required string Email { get; set; }
}

// the caller is now forced to set both
var p = new Person { Name = "Ada", Email = "ada@example.com" };

Das ist oft die beste moderne Antwort für DTOs und Konfigurationsobjekte: kein Boilerplate-Konstruktor, kein falscher Standardwert, und die Nicht-Null-Garantie wird an jeder Aufrufstelle erzwungen. Der Kompromiss ist, dass das Auslassen eines Werts an der Aufrufstelle zu einem Kompilierungsfehler (CS9035) wird statt zu einer Warnung am Typ. Wenn Sie dazu greifen, lesen Sie den ergänzenden Beitrag über CS9035 und erforderliche Mitglieder, damit Sie wissen, wie der aufruferseitige Fehler aussieht.

4. Machen Sie das Mitglied nullbar, wenn null ein gültiger Zustand ist

Kann das Mitglied wirklich fehlen, sollte es string? sein, nicht string. Das Hinzufügen des ? teilt dem Compiler und jedem Leser mit, dass dieser Wert null sein könnte, was ehrlich ist und die Null-Prüfung dorthin verschiebt, wo der Wert konsumiert wird:

// .NET 11, C# 14
public class Person
{
    public string Name { get; set; } = string.Empty;
    public string? MiddleName { get; set; }   // legitimately optional
}

Greifen Sie dazu nicht nur, um die Warnung bei einem Mitglied zum Schweigen zu bringen, das nie wirklich null ist. Ein Mitglied als nullbar zu markieren, obwohl es in der Praxis immer gesetzt ist, schiebt Phantom-Null-Prüfungen (oder !-Null-Vergebungsoperatoren) auf jeden Konsumenten. Reservieren Sie ? für Werte, die tatsächlich optional sind.

5. Annotieren Sie eine Hilfsmethode mit [MemberNotNull] oder verwenden Sie null! für vom Framework initialisierte Mitglieder

Manchmal ist das Mitglied initialisiert, nur nicht an einer Stelle, der der Compiler folgt. Zwei Werkzeuge decken das ab.

Erledigt eine gemeinsame private Methode die Initialisierung, teilen Sie es dem Compiler mit [MemberNotNull] mit:

// .NET 11, C# 14
using System.Diagnostics.CodeAnalysis;

public class Student
{
    public string Major { get; set; }

    public Student() => SetMajor();

    [MemberNotNull(nameof(Major))]
    private void SetMajor(string? major = null) => Major = major ?? "Undeclared";
}

[MemberNotNull] behauptet, dass das genannte Mitglied nach der Rückkehr der Methode nicht null ist, sodass ein Konstruktor, der sie aufruft, als das Mitglied zuweisend gilt. Wie [SetsRequiredMembers] ist dies ein Versprechen, dem der Compiler ungeprüft glaubt, halten Sie es also ehrlich.

Der andere Fall ist ein Mitglied, das ein Framework per Reflexion setzt, der Klassiker ist ein EF-Core-DbSet. Der Basis-DbContext füllt diese, aber der Compiler kann das nicht sehen, daher lautet die Redewendung, mit null! zu initialisieren:

// .NET 11, EF Core 11
public class TodoContext : DbContext
{
    public TodoContext(DbContextOptions<TodoContext> options) : base(options) { }

    public DbSet<TodoItem> TodoItems { get; set; } = null!;
}

Das null! sagt “nimm an, dies ist nicht null; ich weiß, dass es anderswo gesetzt wird.” Es ist eine gezielte Unterdrückung, keine Lösung, verwenden Sie es also nur, wenn etwas außerhalb Ihres Konstruktors die Initialisierung wirklich vornimmt. Dieses Muster taucht im gesamten EF-Core-Code auf; dieselbe Überlegung gilt für Entitäten, die das ORM materialisiert, behandelt in wie man Records mit EF Core 11 korrekt verwendet.

Fallstricke und Varianten

Eine Handvoll Situationen erzeugt CS8618 oder etwas Ähnliches aus Gründen, die die Meldung nicht ausbuchstabiert:

Das mentale Modell, das Sie behalten sollten: CS8618 ist der Compiler, der das Versprechen durchsetzt, das ein nicht-nullbares Mitglied gibt. Wenn Sie es sehen, entscheiden Sie, was tatsächlich zutrifft, und handeln Sie entsprechend. Das Mitglied ist verpflichtend (weisen Sie es in einem Konstruktor zu oder markieren Sie es als required), es hat einen vernünftigen Standard (geben Sie ihm einen Feldinitialisierer), es ist wirklich optional (machen Sie es string?), oder es wird durch Code initialisiert, den der Compiler nicht sehen kann ([MemberNotNull] oder null!). Zu null! bei einem Mitglied zu greifen, das ein Aufrufer setzen soll, verschiebt nur eine Kompilierungswarnung in eine NullReferenceException zur Laufzeit, was genau der Fehler ist, den nullbare Referenztypen verhindern sollen.

Verwandt

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück