Freigeben über


Erforderliche Mitglieder

Anmerkung

Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den relevanten Anmerkungen zum Language Design Meeting (LDM) erfasst.

Weitere Informationen zum Prozess für die Aufnahme von Funktions-Speclets in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Zusammenfassung

Dieser Vorschlag fügt eine Möglichkeit hinzu, anzugeben, dass eine Eigenschaft oder ein Feld während der Objektinitialisierung festgelegt werden muss, wodurch der Ersteller der Instanz einen Anfangswert für das Element in einem Objektinitialisierer am Erstellungsort bereitstellen muss.

Motivation

Objekthierarchien erfordern heute viel Standardcode, um Daten über alle Hierarchieebenen hinweg zu transportieren. Sehen wir uns eine einfache Hierarchie an, die eine Person umfasst, wie möglicherweise in C# 8 definiert:

class Person
{
    public string FirstName { get; }
    public string MiddleName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName, string? middleName = null)
    {
        FirstName = firstName;
        LastName = lastName;
        MiddleName = middleName ?? string.Empty;
    }
}

class Student : Person
{
    public int ID { get; }
    public Student(int id, string firstName, string lastName, string? middleName = null)
        : base(firstName, lastName, middleName)
    {
        ID = id;
    }
}

Hier gibt es viele Wiederholungen:

  1. An der Wurzel der Hierarchie musste der Typ jeder Eigenschaft zweimal wiederholt werden, und der Name musste viermal wiederholt werden.
  2. Auf der abgeleiteten Ebene musste der Typ jeder geerbten Eigenschaft einmal wiederholt werden, und der Name musste zweimal wiederholt werden.

Dies ist eine einfache Hierarchie mit 3 Eigenschaften und 1 Vererbungsebene, aber viele Beispiele dieser Hierarchietypen gehen viel tiefer und sammeln dabei immer mehr Eigenschaften an, die weitergegeben werden. Roslyn ist ein Beispiel für eine solche Codebasis, insbesondere in den verschiedenen Baumtypen, die unsere CSTs und ASTs bilden. Diese Verschachtelung ist so mühsam, dass wir über Codegeneratoren verfügen, um die Konstruktoren und Definitionen dieser Typen zu generieren, und viele Kunden wählen ähnliche Ansätze zur Lösung des Problems. C# 9 führt Datensätze ein, die dies für einige Szenarien verbessern können:

record Person(string FirstName, string LastName, string MiddleName = "");
record Student(int ID, string FirstName, string LastName, string MiddleName = "") : Person(FirstName, LastName, MiddleName);

recordbeseitigt die erste Duplizierungsquelle, aber die zweite Duplizierungsquelle bleibt unverändert: Leider ist dies die Duplizierungsquelle, die größer wird, wenn die Hierarchie wächst und damit der problematischste Teil der Duplizierung, der nach einer Änderung an der Hierarchie behoben werden muss, da es erforderlich ist, die Hierarchie durch alle ihre Standorte zu verfolgen, möglicherweise sogar über Projekte hinweg, was potenziell große Probleme für die Verbraucher mit sich bringen kann.

Als Workaround zur Vermeidung dieser Duplizierung sehen wir seit langem, dass Verbraucher Objektinitialisierer nutzen, um das Schreiben von Konstruktoren zu vermeiden. Vor C# 9 hatte dies jedoch zwei wesentliche Nachteile:

  1. Die Objekthierarchie muss vollständig modifizierbar sein, mit set-Accessoren für jede Eigenschaft.
  2. Es gibt keine Möglichkeit, sicherzustellen, dass jede Instanziierung eines Objekts aus dem Diagramm jedes Element festlegt.

C# 9 hat hier erneut das erste Problem behoben, indem der init Accessor eingeführt wurde: Mit ihm können diese Eigenschaften bei der Objekterstellung/-initialisierung festgelegt werden, jedoch nicht danach. Wir haben jedoch noch einmal das zweite Problem: Eigenschaften in C# sind seit C# 1.0 optional. In C# 8.0 eingeführte Verweistypen mit Null-Möglichkeit haben einen Teil dieses Problems behoben: Wenn ein Konstruktor eine Verweistyp-Eigenschaft ohne Null-Möglichkeit nicht initialisiert, wird der Benutzer dazu gewarnt. Dies löst das Problem jedoch nicht: Der Benutzer möchte hier nicht große Teile seines Typs im Konstruktor wiederholen, sondern er möchte die Anforderung übergeben, um Eigenschaften für die Verbraucher festzulegen. Außerdem werden keine Warnungen zu ID von Student gegeben, da es sich um einen Werttyp handelt. Diese Szenarien sind in ORMs für Datenbankmodelle, wie z.B. EF Core, sehr häufig, die einen öffentlichen parameterlosen Konstruktor benötigen, aber dann die Null-Möglichkeit für die Zeilen von der Null-Möglichkeit der Eigenschaften ableiten müssen.

Dieser Vorschlag zielt darauf ab, diese Bedenken zu beheben, indem ein neues Feature in C# eingeführt wird: erforderliche Mitglieder. Erforderliche Mitglieder müssen von den Verbrauchern initialisiert werden, und nicht vom Typ-Ersteller. Es gibt verschiedene Anpassungen, um Flexibilität für mehrere Konstruktoren und andere Szenarien zu ermöglichen.

Detailliertes Design

Die Typen class, struct und record erhalten die Fähigkeit, eine required_member_list zu deklarieren. Dies ist die Liste aller Eigenschaften und Felder eines Typs, die als erforderlich betrachtet werden und während der Erstellung und Initialisierung einer Instanz des Typs initialisiert werden müssen. Typen übernehmen diese Listen automatisch von ihren Basistypen, was eine nahtlose Umgebung bietet und überflüssigen sowie sich wiederholenden Code beseitigt.

required-Modifizierer

Wir fügen 'required' der Liste der Modifizierer im field_modifier und im property_modifier hinzu. Die required_member_list eines Typs besteht aus allen Elementen, auf die required angewendet wurde. Daher sieht der zuvor erwähnte Person-Typ jetzt so aus:

public class Person
{
    // The default constructor requires that FirstName and LastName be set at construction time
    public required string FirstName { get; init; }
    public string MiddleName { get; init; } = "";
    public required string LastName { get; init; }
}

Alle Konstruktoren für einen Typ, der über eine required_member_list verfügt, kündigen automatisch einen Vertrag an, dass Verbraucher des Typs alle Eigenschaften in der Liste initialisieren müssen. Es ist ein Fehler für einen Konstruktor, einen Vertrag anzukündigen, der ein Mitglied erfordert, das nicht mindestens so zugänglich ist wie der Konstruktor selbst. Zum Beispiel:

public class C
{
    public required int Prop { get; protected init; }

    // Advertises that Prop is required. This is fine, because the constructor is just as accessible as the property initer.
    protected C() {}

    // Error: ctor C(object) is more accessible than required property Prop.init.
    public C(object otherArg) {}
}

required ist nur in class, structund record Typen gültig. Dies gilt nicht in interface-Typen. required kann nicht mit den folgenden Modifizierern kombiniert werden:

  • fixed
  • ref readonly
  • ref
  • const
  • static

required darf nicht für Indexer angewendet werden.

Der Compiler gibt eine Warnung aus, wenn Obsolete auf ein erforderliches Element eines Typs angewendet wird und:

  1. Der Typ ist nicht als Obsolete markiert oder
  2. Jeder Konstruktor, der nicht mit SetsRequiredMembersAttribute versehen ist, ist nicht als Obsolete markiert.

SetsRequiredMembersAttribute

Alle Konstruktoren in einem Typ mit erforderlichen Mitgliedern, oder deren Basistyp erforderliche Mitglieder angibt, müssen diese Mitglieder von einem Verbraucher festlegen lassen, wenn dieser Konstruktor aufgerufen wird. Um Konstruktoren von dieser Anforderung auszunehmen, kann ein Konstruktor mit SetsRequiredMembersAttribute versehen werden, wodurch diese Anforderungen aufgehoben werden. Der Konstruktortext wird nicht überprüft, um sicherzustellen, dass er definitiv die erforderlichen Mitglieder des Typs festlegt.

SetsRequiredMembersAttribute entfernt alle Anforderungen von einem Konstruktor, und diese Anforderungen werden in keiner Weise auf ihre Gültigkeit überprüft. NB: Dies ist der Escape-Hatch, wenn die Übernahme von einem Typ mit einer ungültigen Liste erforderlicher Mitgliedern nötig ist: Markieren Sie den Konstruktor dieses Typs mit SetsRequiredMembersAttribute, und es werden keine Fehler gemeldet.

Wenn ein Konstruktor C mit einem base oder this Konstruktor, der mit SetsRequiredMembersAttribute markiert ist, verkettet ist, muss C auch mit SetsRequiredMembersAttribute markiert werden.

Bei einem Datensatztyp wird SetsRequiredMembersAttribute für den synthetisierten Kopierkonstruktor eines Datensatzes ausgegeben, wenn der Datensatztyp oder einer seiner Basistypen erforderliche Mitglieder hat.

NB: Eine frühere Version dieses Vorschlags verwendete eine umfassendere Metasprache rund um die Initialisierung, die es ermöglichte, einzelne erforderliche Mitglieder in einem Konstruktor hinzuzufügen oder zu entfernen, sowie die Überprüfung, dass der Konstruktor alle erforderlichen Mitglieder festlegt. Dies gilt als zu komplex für die erste Version und wurde entfernt. Wir können uns das Hinzufügen komplexerer Verträge und Änderungen als späteres Feature ansehen.

Durchsetzung

Für jeden Konstruktor Ci in Typ T mit erforderlichen Mitgliedern R müssen Verbraucher, die Ci aufrufen, eine der folgenden Aktionen durchführen:

  • Setzen Sie alle Mitglieder von R in einem object_initializer auf die object_creation_expression,
  • Oder setzen Sie alle Mitglieder von R über den Abschnitt named_argument_list eines attribute_target fest.

es sei denn, Ci ist mit SetsRequiredMembers markiert.

Wenn der aktuelle Kontext keinen object_initializer zulässt, kein attribute_target ist und Ci nicht mit SetsRequiredMembers markiert ist, führt der Aufruf von Ci zu einem Fehler.

new() Einschränkung

Ein Typ mit einem parameterlosen Konstruktor, der einen Vertrag ankündigt, darf nicht als Typparameter verwendet werden, der auf new() beschränkt ist, da es keine Möglichkeit für die generische Instanziierung gibt, sicherzustellen, dass die Anforderungen erfüllt sind.

struct defaults

Erforderliche Mitglieder werden nicht für Instanzen von struct-Typen erzwungen, die mit default oder default(StructType) erstellt wurden. Sie werden für struct-Instanzen erzwungen, die mit new StructType() erstellt wurden, selbst wenn StructType keinen parameterlosen Konstruktor hat und der Standard-Strukturkonstruktor verwendet wird.

Zugänglichkeit

Es führt zu einem Fehler, ein Element zu markieren, das erforderlich ist, wenn das Element in keinem Kontext festgelegt werden kann, in dem der enthaltende Typ sichtbar ist.

  • Wenn das Mitglied ein Feld ist, kann es nicht readonly sein.
  • Wenn es sich bei dem Element um eine Eigenschaft handelt, muss es über einen Setter oder Initer verfügen oder mindestens so zugänglich sein wie der enthaltende Typ des Elements.

Dies bedeutet, dass die folgenden Fälle nicht zulässig sind:

interface I
{
    int Prop1 { get; }
}
public class Base
{
    public virtual int Prop2 { get; set; }

    protected required int _field; // Error: _field is not at least as visible as Base. Open question below about the protected constructor scenario

    public required readonly int _field2; // Error: required fields cannot be readonly
    protected Base() { }

    protected class Inner
    {
        protected required int PropInner { get; set; } // Error: PropInner cannot be set inside Base or Derived
    }
}
public class Derived : Base, I
{
    required int I.Prop1 { get; } // Error: explicit interface implementions cannot be required as they cannot be set in an object initializer

    public required override int Prop2 { get; set; } // Error: this property is hidden by Derived.Prop2 and cannot be set in an object initializer
    public new int Prop2 { get; }

    public required int Prop3 { get; } // Error: Required member must have a setter or initer

    public required int Prop4 { get; internal set; } // Error: Required member setter must be at least as visible as the constructor of Derived
}

Es ist ein Fehler, ein required Mitglied auszublenden, da dieses Mitglied nicht mehr von einem Verbraucher festgelegt werden kann.

Beim Überschreiben eines required-Elements muss das Schlüsselwort required in die Methodensignatur eingeschlossen werden. Dies geschieht, damit wir, falls wir in Zukunft jemals zulassen wollen, dass eine Eigenschaft mit einer Überschreibung nicht mehr erforderlich ist, den erforderlichen Gestaltungsspielraum dafür haben.

Überschreibungen dürfen ein Element required markieren, wo es im Basistyp nicht vorhanden war required. Ein so markiertes Mitglied wird der Liste der erforderlichen Mitglieder des abgeleiteten Typs hinzugefügt.

Typen dürfen erforderliche virtuelle Eigenschaften außer Kraft setzen. Dies bedeutet: Wenn die virtuelle Basiseigenschaft über Speicher verfügt und der abgeleitete Typ versucht, auf die Basisimplementierung dieser Eigenschaft zuzugreifen, trifft er möglicherweise auf nicht initialisierten Speicher. NB: Dies ist ein allgemeines C#-Antimuster, und wir glauben nicht, dass dieser Vorschlag versuchen sollte, es zu beheben.

Auswirkung auf die Analyse der Null-Möglichkeit

Mitglieder, die als required markiert sind, müssen nicht am Ende eines Konstruktors in einen gültigen Zustand mit Null-Möglichkeit initialisiert werden. Alle required Members dieses Typs und alle Basistypen werden von der Analyse der Null-Möglichkeit als Standard am Anfang eines Konstruktors in diesem Typ berücksichtigt, es sei denn, es besteht eine Verkettung mit einem this- oder base-Konstruktor, der mit SetsRequiredMembersAttribute markiert ist.

Die Analyse der Null-Möglichkeit warnt bei allen required Mitgliedern aus den aktuellen und Basistypen, die keinen gültigen Null-Möglichkeits-Zustand am Ende eines Konstruktors aufweisen, der mit SetsRequiredMembersAttribute markiert ist.

#nullable enable
public class Base
{
    public required string Prop1 { get; set; }

    public Base() {}

    [SetsRequiredMembers]
    public Base(int unused) { Prop1 = ""; }
}
public class Derived : Base
{
    public required string Prop2 { get; set; }

    [SetsRequiredMembers]
    public Derived() : base()
    {
    } // Warning: Prop1 and Prop2 are possibly null.

    [SetsRequiredMembers]
    public Derived(int unused) : base()
    {
        Prop1.ToString(); // Warning: possibly null dereference
        Prop2.ToString(); // Warning: possibly null dereference
    }

    [SetsRequiredMembers]
    public Derived(int unused, int unused2) : this()
    {
        Prop1.ToString(); // Ok
        Prop2.ToString(); // Ok
    }

    [SetsRequiredMembers]
    public Derived(int unused1, int unused2, int unused3) : base(unused1)
    {
        Prop1.ToString(); // Ok
        Prop2.ToString(); // Warning: possibly null dereference
    }
}

Metadatendarstellung

Die folgenden 2 Attribute sind für den C#-Compiler bekannt und erforderlich, damit dieses Feature funktioniert:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
    public sealed class RequiredMemberAttribute : Attribute
    {
        public RequiredMemberAttribute() {}
    }
}

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
    public sealed class SetsRequiredMembersAttribute : Attribute
    {
        public SetsRequiredMembersAttribute() {}
    }
}

Es ist ein Fehler, RequiredMemberAttribute manuell auf einen Typ anzuwenden.

Auf jedes Mitglied, das mit required markiert ist, wird RequiredMemberAttribute angewendet. Darüber hinaus wird jeder Typ, der solche Mitglieder definiert, mit RequiredMemberAttributegekennzeichnet, um anzugeben, dass in diesem Typ erforderliche Mitglieder vorhanden sind. Beachten Sie: Wenn der Typ B von A abgeleitet ist und A required Mitglieder definiert, aber B keine neuen hinzufügt oder vorhandene required Mitglieder außer Kraft setzt, wird B nicht mit RequiredMemberAttribute markiert. Um vollständig zu bestimmen, ob in Berforderliche Elemente vorhanden sind, ist die Überprüfung der vollständigen Vererbungshierarchie erforderlich.

Jeder Konstruktor in einem Typ, der required-Mitglieder enthält und auf den SetsRequiredMembersAttribute nicht angewendet wird, ist mit zwei Attributen markiert:

  1. System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute mit dem Funktionsnamen "RequiredMembers".
  2. System.ObsoleteAttribute mit der Zeichenfolge "Types with required members are not supported in this version of your compiler", und das Attribut wird als Fehler markiert, um zu verhindern, dass alte Compiler diese Konstruktoren verwenden.

Wir verwenden hier keine modreq, da es darum geht, die Binär-Kompatibilität zu wahren: Wenn die letzte required-Eigenschaft von einem Typ entfernt wurde, würde der Compiler diese modreq nicht mehr synthetisieren, was eine disruptive Binär-Änderung darstellt, wodurch alle bestehenden Verbraucher neu kompiliert werden müssten. Ein Compiler, der required-Mitglieder versteht, wird dieses veraltete Attribut ignorieren. Beachten Sie, dass Mitglieder auch aus Basistypen stammen können: Auch wenn im aktuellen Typ keine neuen required Mitglieder vorhanden sind, wird dieses Obsolete Attribut generiert, wenn ein Basistyp required Mitglieder enthält. Wenn der Konstruktor bereits über ein Obsolete-Attribut verfügt, wird kein zusätzliches Obsolete-Attribut generiert.

Wir verwenden sowohl ObsoleteAttribute als auch CompilerFeatureRequiredAttribute, da letzteres neu ist, und ältere Compiler verstehen sie nicht. In Zukunft können wir möglicherweise die ObsoleteAttribute entfernen und/oder nicht verwenden, um neue Funktionen zu schützen, derzeit brauchen wir aber beide für den vollen Schutz.

Zum Erstellen der vollständigen Liste der required Mitglieder R für einen bestimmten Typ T, einschließlich aller Basistypen, wird der folgende Algorithmus ausgeführt:

  1. Für alle Tb, beginnend mit T und durch die Basistypkette hindurch, bis object erreicht ist.
  2. Wenn Tb mit RequiredMemberAttributegekennzeichnet ist, werden alle Mitglieder von Tb, die mit RequiredMemberAttribute gekennzeichnet sind, in Rb gesammelt.
    1. Für alle Ri in Rb gilt: Wenn Ri von einem Mitglied von R überschrieben wird, wird es übersprungen.
    2. Andernfalls gilt: Wenn ein Ri durch ein Mitglied von R verborgen wird, schlägt die Suche nach den erforderlichen Mitgliedern fehl, und es werden keine weiteren Schritte mehr durchgeführt. Wenn ein Konstruktor von T ohne Markierung mit SetsRequiredMembers aufgerufen wird, wird ein Fehler ausgegeben.
    3. Andernfalls wird Ri zu Rhinzugefügt.

Offene Fragen

Verschachtelte Mitgliedsinitialisierer

Wie werden die Erzwingungsmechanismen für verschachtelte Mitgliedsinitialisierer aussehen? Werden sie völlig verboten sein?

class Range
{
    public required Location Start { get; init; }
    public required Location End { get; init; }
}

class Location
{
    public required int Column { get; init; }
    public required int Line { get; init; }
}

_ = new Range { Start = { Column = 0, Line = 0 }, End = { Column = 1, Line = 0 } } // Would this be allowed if Location is a struct type?
_ = new Range { Start = new Location { Column = 0, Line = 0 }, End = new Location { Column = 1, Line = 0 } } // Or would this form be necessary instead?

Besprochene Fragen

Erzwingungsstufe für init-Klauseln

In C# 11 wurde das Feature der init-Klausel nicht implementiert. Es bleibt ein aktiver Vorschlag.

Erzwingen wir strikt, dass Mintglieder, die in einer init-Klausel ohne Initialisierer angegeben sind, alle Mitglieder initialisieren? Es scheint wahrscheinlich, dass wir dies tun, andernfalls führt dies schnell zu Problemen. Wir laufen jedoch auch Gefahr, die gleichen Probleme wieder einzuführen, die wir mit MemberNotNull in C# 9 gelöst haben. Wenn wir dies strikt erzwingen wollen, benötigen wir wahrscheinlich eine Möglichkeit für eine Hilfsmethode, um anzugeben, dass ein Mitglied gesetzt wird. Einige Syntaxmöglichkeiten, die wir dafür besprochen haben:

  • Zulassen von init-Methoden. Diese Methoden dürfen nur von einem Konstruktor oder einer anderen init-Methode aufgerufen werden und können auf this zugreifen, als ob sie sich im Konstruktor befänden (d.h. Setzen der readonly- und init-Felder/Eigenschaften). Dies kann mit init-Klauseln für derartige Methoden kombiniert werden. Eine init-Klausel würde als erfüllt betrachtet werden, wenn das Mitglied in der Klausel definitiv im Textkörper der Methode/des Konstruktors zugewiesen ist. Das Aufrufen einer Methode mit einer init-Klausel, die ein Element enthält, zählt als Zuweisung zu diesem Element. Wenn wir uns entscheiden, dass dies ein Weg ist, den wir jetzt oder in Zukunft verfolgen möchten, scheint es wahrscheinlich, dass wir init nicht als Schlüsselwort für die Initialisierungsklausel eines Konstruktors verwenden sollten, da dies verwirrend wäre.
  • Zulassen, dass der !-Operator die Warnung/den Fehler explizit unterdrückt. Wenn ein Mitglied auf komplizierte Weise initialisiert wird (z. B. in einer freigegebenen Methode), kann der Benutzer der Initialisierungsklausel eine ! hinzufügen, um anzugeben, dass der Compiler die Initialisierung nicht überprüft.

Schlussfolgerung: Nach der Diskussion gefiel uns das Konzept des !-Operators. Dieser ermöglicht dem Benutzer, absichtsvoll mit komplizierteren Szenarien umzugehen, während gleichzeitig keine großen Designlücken um init-Methoden herum entstehen und nicht jede Methode als Steuerung eines Mitglieds X oder Y kommentiert werden muss. ! wurde ausgewählt, da wir dies bereits zum Unterdrücken von Warnungen mit Null-Möglichkeit verwenden und um dem Compiler an anderer Stelle mitzuteilen, dass „Ich bin klüger als du“ an anderer Stelle eine natürliche Erweiterung der Syntax ist.

Erforderliche Schnittstellenmitglieder

Dieser Vorschlag ermöglicht nicht, dass Schnittstellen Mitglieder als erforderlich kennzeichnen. Dies schützt uns davor, uns mit komplexen Szenarien rund um new() und Schnittstelleneinschränkungen bei Generika auseinandersetzen zu müssen und steht in direktem Zusammenhang mit sowohl Fabriken als auch generischen Konstruktionen. Um sicherzustellen, dass wir in diesem Bereich Freiraum für Entwürfe haben, untersagen wir required in Schnittstellen und verbieten, dass Typen mit required_member_lists als Ersatz für Typparameter verwendet werden, die auf new() beschränkt sind. Wenn wir einen umfassenderen Blick auf generische Bauszenarien mit Fabriken werfen möchten, können wir dieses Problem erneut überprüfen.

Syntaxfragen

Die init-Klausel-Funktion wurde in C# 11 nicht implementiert. Es bleibt ein aktiver Vorschlag.

  • Ist init das richtige Wort? init als Postfix-Modifizierer des Konstruktors könnte stören, wenn wir dies jemals für Fabriken wiederverwenden möchten und auch init-Methoden mit einem Präfixmodifizierer ermöglichen wollen. Weitere Möglichkeiten:
    • set
  • Ist required der richtige Modifizierer, um anzugeben, dass alle Mitglieder initialisiert werden? Andere haben folgendes vorgeschlagen:
    • default
    • all
    • Mit einem ! zur Anzeige komplexer Logik
  • Sollten wir ein Trennzeichen zwischen dem base/this und dem initbenötigen?
    • : Trennzeichen
    • „,“-Trennzeichen
  • Ist required der richtige Modifizierer? Andere Alternativen, die vorgeschlagen wurden:
    • req
    • require
    • mustinit
    • must
    • explicit

Schlussfolgerung: Wir haben die init-Konstruktorklausel vorerst entfernt und werden mit required als Eigenschaftsmodifizierer fortfahren.

Einschränkungen für Init-Klausel

Das Feature der init-Klausel wurde in C# 11 nicht implementiert. Es bleibt ein aktiver Vorschlag.

Sollen wir den Zugriff auf this in der Init-Klausel zulassen? Wenn wir möchten, dass die Zuordnung in init eine Kurzform zum Zuweisen des Mitglieds im Konstruktor selbst sein soll, sollten wir dies wohl tun.

Wird darüber hinaus ein neuer Bereich erstellt, wie bei base(), entspricht der Umfang dem des Methodentexts? Dies ist besonders wichtig für Dinge wie lokale Funktionen, auf die die Init-Klausel zugreifen könnte, oder für Name Shadowing, wenn ein Init-Ausdruck eine Variable über den out-Parameter einführt.

Schlussfolgerung: Die init-Klausel wurde entfernt.

Anforderungen an Zugänglichkeit und init

Die init-Klausel-Funktion wurde in C# 11 nicht implementiert. Es bleibt ein aktiver Vorschlag.

In Versionen dieses Vorschlags mit der init-Klausel haben wir darüber gesprochen, dass wir das folgende Szenario haben können:

public class Base
{
    protected required int _field;

    protected Base() {} // Contract required that _field is set
}
public class Derived : Base
{
    public Derived() : init(_field = 1) // Contract is fulfilled and _field is removed from the required members list
    {
    }
}

Wir haben jedoch die init Klausel an diesem Punkt aus dem Vorschlag entfernt, daher müssen wir entscheiden, ob dieses Szenario in begrenztem Umfang zulässig ist. Folgende Optionen stehen zur Auswahl:

  1. Das Szenario verbieten. Dies ist der vorsichtigste Ansatz, und die Regeln für Zugänglichkeit sind derzeit mit dieser Annahme geschrieben. Die Regel besteht darin, dass jedes erforderliche Mitglied mindestens so sichtbar sein muss wie der enthaltende Typ.
  2. Es ist zu erfordern, dass für alle Konstruktoren eine der folgenden Bedingungen gilt:
    1. Nicht sichtbarer als das am wenigsten sichtbare erforderliche Mitglied.
    2. Anwendung von SetsRequiredMembersAttribute auf den Konstruktor. Dadurch würde sichergestellt, dass alle, der einen Konstruktor sehen können, entweder alle exportierten Elemente festlegen können, oder dass nichts festgelegt werden kann. Dies könnte für Typen nützlich sein, die nur über statische Create-Methoden oder ähnliche Generatoren erstellt werden, allerdings ist der Nutzen insgesamt nur begrenzt.
  3. Fügen Sie dem Vorschlag eine Möglichkeit hinzu, um spezifische Teile des Vertrags zu entfernen, wie bereits in LDM besprochen.

Schlussfolgerung: Option 1, alle erforderlichen Mitglieder müssen mindestens so sichtbar sein wie der sie enthaltende Typ.

Überschreibungsregeln

Die aktuelle Spezifikation besagt, dass das Schlüsselwort required kopiert werden muss, und dass Überschreibungen ein Mitglied mehr erforderlich machen können, aber nicht weniger. Wollen wir das tun? Das Entfernen von Anforderungen erfordert mehr Möglichkeiten für Vertragsänderungen, als wir derzeit vorschlagen.

Schlussfolgerung: Das Hinzufügen von required bei Überschreibungen ist zulässig. Wenn das überschriebene Element required ist, muss das überschreibende Element auch required sein.

Alternative Metadatendarstellung

Wir könnten auch einen anderen Ansatz für die Darstellung von Metadaten verfolgen und dabei von Erweiterungsmethoden lernen. Wir könnten einen RequiredMemberAttribute für den Typ verwenden, um anzugeben, dass der Typ erforderliche Mitglieder enthält, und dann eine RequiredMemberAttribute bei jedem Mitglied, das erforderlich ist. Dies würde die Suchsequenz vereinfachen (es wäre keine Mitgliedersuche erforderlich, sondern nur eine Suche nach Mitgliedern mit dem Attribut).

Schlussfolgerung: Alternative genehmigt.

Metadatendarstellung

Die Metadatendarstellung muss genehmigt werden. Darüber hinaus müssen wir entscheiden, ob diese Attribute in die BCL aufgenommen werden sollen.

  1. Bei RequiredMemberAttribute ähnelt dieses Attribut den allgemeinen eingebetteten Attributen, die wir für nullable/nint/tuple-Mitgliedsnamen verwenden, und wird vom Benutzer in C# nicht manuell angewendet. Es ist möglich, dass andere Sprachen dieses Attribut jedoch manuell anwenden möchten.
  2. SetsRequiredMembersAttribute hingegen wird direkt von Verbrauchern genutzt und sollte daher vermutlich zur BCL gehören.

Wenn wir die alternative Darstellung im vorherigen Abschnitt verwenden, könnte dies die Berechnung für RequiredMemberAttribute verändern: Anstatt den allgemeinen eingebetteten Attributen für nint/nullable/tuple-Mitgliedsnamen zu ähneln, ist es näher an System.Runtime.CompilerServices.ExtensionAttribute, das sich seit der Einführung von Erweiterungsmethoden im Framework befindet.

Schlussfolgerung: Wir werden beide Attribute in die BCL aufnehmen.

Warnung vs Fehler

Sollte das Festlegen eines erforderlichen Mitglieds nicht zu einer Warnung oder einem Fehler führen? Es ist sicherlich möglich, das System über Activator.CreateInstance(typeof(C)) oder Ähnliches auszutricksen, was bedeutet, dass wir möglicherweise nicht in der Lage sind, immer alle Eigenschaften vollständig sicherzustellen. Wir ermöglichen auch die Unterdrückung der Diagnose an der Konstruktorstelle mittels !, was wir im Allgemeinen für Fehler nicht zulassen. Das Feature ähnelt jedoch schreibgeschützten Feldern oder Init-Eigenschaften, da ein schwerwiegender Fehler ausgelöst wird, wenn Benutzer versuchen, ein solches Mitglied nach der Initialisierung festzulegen. Dennoch können sie durch Reflexion umgangen werden.

Schlussfolgerung: Fehler.