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:
- An der Wurzel der Hierarchie musste der Typ jeder Eigenschaft zweimal wiederholt werden, und der Name musste viermal wiederholt werden.
- 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);
record
beseitigt 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:
- Die Objekthierarchie muss vollständig modifizierbar sein, mit
set
-Accessoren für jede Eigenschaft. - 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
, struct
und 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:
- Der Typ ist nicht als
Obsolete
markiert oder - Jeder Konstruktor, der nicht mit
SetsRequiredMembersAttribute
versehen ist, ist nicht alsObsolete
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
default
s
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 RequiredMemberAttribute
gekennzeichnet, 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 B
erforderliche 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:
-
System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute
mit dem Funktionsnamen"RequiredMembers"
. -
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:
- Für alle
Tb
, beginnend mitT
und durch die Basistypkette hindurch, bisobject
erreicht ist. - Wenn
Tb
mitRequiredMemberAttribute
gekennzeichnet ist, werden alle Mitglieder vonTb
, die mitRequiredMemberAttribute
gekennzeichnet sind, inRb
gesammelt.- Für alle
Ri
inRb
gilt: WennRi
von einem Mitglied vonR
überschrieben wird, wird es übersprungen. - Andernfalls gilt: Wenn ein
Ri
durch ein Mitglied vonR
verborgen wird, schlägt die Suche nach den erforderlichen Mitgliedern fehl, und es werden keine weiteren Schritte mehr durchgeführt. Wenn ein Konstruktor vonT
ohne Markierung mitSetsRequiredMembers
aufgerufen wird, wird ein Fehler ausgegeben. - Andernfalls wird
Ri
zuR
hinzugefügt.
- Für alle
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 andereninit
-Methode aufgerufen werden und können aufthis
zugreifen, als ob sie sich im Konstruktor befänden (d.h. Setzen derreadonly
- undinit
-Felder/Eigenschaften). Dies kann mitinit
-Klauseln für derartige Methoden kombiniert werden. Eineinit
-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 einerinit
-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 wirinit
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 auchinit
-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 deminit
benö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:
- 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.
- Es ist zu erfordern, dass für alle Konstruktoren eine der folgenden Bedingungen gilt:
- Nicht sichtbarer als das am wenigsten sichtbare erforderliche Mitglied.
- 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 statischeCreate
-Methoden oder ähnliche Generatoren erstellt werden, allerdings ist der Nutzen insgesamt nur begrenzt.
- 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.
- 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. -
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.
C# feature specifications