Start Debugging

Lösung: [MemberNotNull] wirkt nicht auf einer statischen Factory-Methode und CS8618 bleibt bestehen

CS8618 wird beim Verlassen des Konstruktors geprüft, und [MemberNotNull] auf einer statischen Methode wird für Instanzmember stillschweigend ignoriert. Übergeben Sie die Werte an einen privaten Konstruktor.

[MemberNotNull] auf einer statischen Factory-Methode kann CS8618 nicht beseitigen, denn die Warnung wird am Konstruktor gemeldet, in dem Moment, in dem der Konstruktor endet, und der Compiler schaut nie darauf, was die Factory danach tut. Dazu kommt: Eine statische Methode hat kein this. Wenn ihr [MemberNotNull] einen Instanzmember nennt, ignoriert der Compiler das Attribut stillschweigend: keine Warnung an der Methode, keine Wirkung für Aufrufer. Die Lösung besteht darin, die Zuweisung zurück in einen Konstruktor zu verlegen: Die Factory berechnet die Werte und übergibt sie an einen privaten Konstruktor, oder die Member werden als required markiert und in einem Objektinitialisierer innerhalb der Factory gesetzt. Verifiziert mit dem .NET 10 SDK 10.0.302 (C# 14); das Verhalten ist dasselbe, seit [MemberNotNull] mit C# 9 kam.

Der Fehler im Kontext

Die Build-Ausgabe zeigt auf den privaten Konstruktor, nicht auf die Factory, die Sie annotiert haben:

Widget.cs(8,13): warning CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Es gibt keine zweite Diagnose, die meldet, dass das Attribut ignoriert wurde. Genau diese Stille macht es verwirrend: Das [MemberNotNull(nameof(Name))] sieht aus, als müsste es etwas bewirken, und nichts im Build sagt etwas anderes.

Warum das passiert

Hier kommen zwei getrennte Regeln zusammen.

CS8618 wird pro Konstruktor geprüft, an dessen Ende. Der Compiler führt die Nullable-Flussanalyse über jeden Konstruktorrumpf aus (plus Feld- und Eigenschaftsinitialisierer) und stellt an der schließenden Klammer genau eine Frage: Ist jeder nicht nullbare Referenzmember definitiv mit einem Nicht-null-Wert belegt? Dem Objekt folgt er nach dem Ende des Konstruktors nicht. Eine Factory, die var w = new Widget(); w.Name = name; return w; ausführt, weist Name zu, nachdem der Konstruktor bereits mit Name gleich null geendet hat, und an diesem Ende sitzt die Warnung. Das Roslyn-Team hat früh entschieden, Initialisierung nicht über Methodenaufrufe hinweg zu verfolgen (“We’ve decided not to track initialization interprocedurally for now” in dotnet/roslyn#32358). [MemberNotNull] ist der optionale Ausweg für genau einen Fall: einen Helper, der innerhalb des Konstruktors auf this aufgerufen wird.

[MemberNotNull] auf einer statischen Methode hat nur für statische Member eine Bedeutung. Das Attribut ist eine Nachbedingung: “Wenn diese Methode zurückkehrt, sind diese Member des Empfängers nicht null.” Bei einer Instanzmethode ist der Empfänger this (oder das Objekt, auf dem Sie sie aufgerufen haben). Eine statische Methode hat keinen Empfänger, also kann sie nur für statische Felder und Eigenschaften bürgen. Wenn Sie einen Instanzmember angeben, akzeptiert Roslyn das nameof (es kompiliert, weil nameof(Name) überall in der Klasse gültig ist) und verwirft die Annotation dann. Beide Hälften habe ich mit SDK 10.0.302 bestätigt: Eine statische Methode mit [MemberNotNull(nameof(Nick))], die Nick nie zuweist, erzeugt kein CS8774, und eine Instanzmethode, die sie aufruft, bekommt beim anschließenden Dereferenzieren von Nick trotzdem CS8602. Dasselbe Attribut mit einem statischen Feld wird dagegen geprüft und berücksichtigt.

Die statische Factory scheitert also doppelt: Sie ist nicht der Konstruktor, und ihre Annotation nennt Member, die sie nicht beschreiben kann.

Minimale Reproduktion

// .NET 10 SDK 10.0.302, C# 14, <Nullable>enable</Nullable>
using System.Diagnostics.CodeAnalysis;

public class Widget
{
    public string Name { get; set; }

    private Widget() { }                     // CS8618 is reported here

    [MemberNotNull(nameof(Name))]            // silently ignored: static method, instance member
    public static Widget Create(string name)
    {
        var w = new Widget();
        w.Name = name;
        return w;
    }
}

Die zweite Form desselben Fehlers ist ein statischer Helper, der die Instanz als Parameter erhält und aus dem Konstruktor aufgerufen wird. Er sieht aus wie das dokumentierte Muster, ist es aber nicht:

// .NET 10 SDK 10.0.302, C# 14
using System.Diagnostics.CodeAnalysis;

public class Gadget
{
    public string Name { get; set; }

    public Gadget(string name) => Init(this, name);   // CS8618 still fires here

    [MemberNotNull(nameof(Name))]
    private static void Init(Gadget g, string name) => g.Name = name;
}

[MemberNotNull] beschreibt den Empfänger des Aufrufs, und Init(this, name) hat keinen Empfänger. this als Argument zu übergeben zählt nicht. Die einzige statische Variante, die funktioniert, ist die, bei der Helper und Member beide statisch sind, etwa ein statischer Konstruktor, der einen statischen Loader aufruft:

// .NET 10 SDK 10.0.302, C# 14
using System.Diagnostics.CodeAnalysis;

public static class Config
{
    public static string ConnectionString;

    static Config() => Load();                 // no CS8618: static helper, static member

    [MemberNotNull(nameof(ConnectionString))]
    private static void Load() => ConnectionString = "Server=.;Database=app";
}

Die Lösung im Detail

Alle folgenden Lösungen verlegen die Zuweisung dorthin, wo CS8618 sie sehen kann: in einen Konstruktor oder hinter einen Vertrag, den der Compiler versteht. Sie sind nach Präferenz geordnet.

1. Berechnete Werte an einen privaten Konstruktor übergeben

Das ist die richtige Form für fast jede Factory. Die Factory erledigt die Arbeit (Validierung, Parsing, I/O) und übergibt fertige Werte an einen Konstruktor, der nur zuweist:

// .NET 10 SDK 10.0.302, C# 14
public sealed class Widget
{
    public string Name { get; }

    private Widget(string name) => Name = name;

    public static Widget Create(string raw)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(raw);
        return new Widget(raw.Trim());
    }
}

Kein Attribut, keine Unterdrückung, und Name kann nur lesbar werden, was eine Factory nach dem Muster “erst konstruieren, dann zuweisen” nie bieten könnte. Bei mehreren Membern bekommt der Konstruktor einfach mehr Parameter. Ein Record mit privatem positionalem Konstruktor ist die kompakte Variante derselben Idee.

2. Den statischen Helper in eine Funktion mit Rückgabewert umbauen

Für die Form Init(this, ...) ändern Sie den Helper so, dass er das Berechnete zurückgibt, und lassen den Konstruktor zuweisen:

// .NET 10 SDK 10.0.302, C# 14
public sealed class Gadget
{
    public string Name { get; }

    public Gadget(string raw) => Name = Normalize(raw);

    private static string Normalize(string raw) => raw.Trim().ToLowerInvariant();
}

Eine statische Methode mit Rückgabewert ist leichter zu testen als eine, die einen Parameter verändert, und der Compiler sieht eine einfache Zuweisung im Konstruktor. Liefert der Helper mehrere Werte, geben Sie ein Tupel zurück und dekonstruieren es: (Name, Slug) = Parse(raw); ist eine definitive Zuweisung beider Member.

3. Den Helper zur Instanzmethode machen und aus dem Konstruktor aufrufen

Ist die Initialisierung wirklich ein Block gemeinsam genutzten imperativen Codes, bleibt sie in einem Helper, aber als Instanzmethode, damit [MemberNotNull] this beschreibt:

// .NET 10 SDK 10.0.302, C# 14
using System.Diagnostics.CodeAnalysis;

public sealed class Report
{
    public string Title { get; private set; }

    private Report(string raw) => Normalize(raw);

    [MemberNotNull(nameof(Title))]
    private void Normalize(string raw) => Title = raw.Trim();

    public static Report Create(string raw) => new(raw);
}

Das ist das Muster, das die Microsoft-Learn-Seite zu den Nullable-Attributen dokumentiert. Anders als im statischen Fall prüft der Compiler den Helper: Kann Normalize zurückkehren, ohne Title zuzuweisen, erhalten Sie CS8774: Member 'Title' must have a non-null value when exiting.

Eine Falle: Ruft die Factory den Instanz-Helper auf dem neuen Objekt auf (var r = new Report(); r.Normalize(raw);), ist r.Title für den Rest der Factory zwar nicht null, weil [MemberNotNull] für jeden Empfänger gilt, auf dem Sie die Methode aufrufen. Für CS8618 ändert das trotzdem nichts, denn der Konstruktor war bereits beendet, bevor diese Zeile lief.

4. Member als required markieren und in der Factory einen Objektinitialisierer verwenden

Wer Konstruktion im Eigenschaftsstil bevorzugt, verschiebt mit required die Pflicht vom Konstruktor auf den Objekterstellungsausdruck, und die Factory ist der einzige Ort, der eine Instanz erzeugen kann:

// .NET 10 SDK 10.0.302, C# 14
public sealed class Invoice
{
    public required string Number { get; init; }
    public required string Customer { get; init; }

    private Invoice() { }

    public static Invoice Create(string number, string customer) =>
        new() { Number = number.Trim(), Customer = customer.Trim() };
}

CS8618 verschwindet, weil ein required-Member nicht in der Verantwortung des Konstruktors liegt. Dazu gibt es ein Sicherheitsnetz, das die ursprüngliche Factory nie hatte: Fehlt einer der Member, schlägt der Build mit CS9035: Required member 'Invoice.Customer' must be set in the object initializer or attribute constructor. fehl. Das ist ein Fehler, keine Warnung. Die CS9035-Anleitung behandelt die Aufruferseite dieses Vertrags.

5. [SetsRequiredMembers] auf einem privaten Konstruktor verwenden

Hat der Typ required-Member für öffentliche Aufrufer, die Factory möchte die Werte aber positional übergeben, fügen Sie einen Konstruktor hinzu, der verspricht, alle zu setzen:

// .NET 10 SDK 10.0.302, C# 14
using System.Diagnostics.CodeAnalysis;

public sealed class Tag
{
    public required string Value { get; init; }

    public Tag() { }

    [SetsRequiredMembers]
    private Tag(string value) => Value = value;

    public static Tag Create(string raw) => new(raw.Trim().ToLowerInvariant());
}

Wie [MemberNotNull] wird auch [SetsRequiredMembers] vertraut, nicht verifiziert: Vergisst dieser Konstruktor einen Member, warnt nichts. Halten Sie den Konstruktor so kurz, dass er sich mit bloßem Auge prüfen lässt.

6. Letzter Ausweg: am Konstruktor unterdrücken, nicht an der Eigenschaft

Wird der Typ von einem Framework erzeugt, das einen parameterlosen Konstruktor verlangt und die Member danach füllt, beschränken Sie die Unterdrückung auf diesen Konstruktor, damit die Analyse an keiner anderen Stelle verloren geht:

// .NET 10 SDK 10.0.302, C# 14
public sealed class LegacyRow
{
    public string Name { get; private set; }

#pragma warning disable CS8618 // Name is always assigned by Create before the instance escapes
    private LegacyRow() { }
#pragma warning restore CS8618

    public static LegacyRow Create(string name) => new() { Name = name };
}

Das ist ehrlich in Bezug auf das, was passiert, und der Kommentar sagt dem nächsten Leser, welche Methode das Versprechen hält. Ziehen Sie das einem = null! an der Eigenschaft vor: null! schaltet die Warnung für jeden Konstruktor stumm, den der Typ je haben wird, auch für später hinzugefügte, die tatsächlich vergessen zuzuweisen.

Fallstricke und Varianten

Verwandte Artikel

Quellen

Comments

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

< Zurück