Freigeben über


Hauptkonstruktoren

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 entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.

Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion Issue: https://github.com/dotnet/csharplang/issues/2691

Zusammenfassung

Klassen und Strukturen können eine Parameterliste aufweisen, und ihre Basisklassenspezifikation kann eine Argumentliste aufweisen. Primäre Konstruktorparameter befinden sich innerhalb der gesamten Klassen- oder Strukturdeklaration im Gültigkeitsbereich, und wenn sie von einem Funktionsmitglied oder einer anonymen Funktion erfasst werden, werden sie entsprechend gespeichert (z. B. als nicht zugängliche private Felder der deklarierten Klasse oder Struktur).

Mit dem Vorschlag werden die primären Konstruktoren, die bereits für Records verfügbar sind, im Sinne dieser allgemeineren Funktion mit einigen zusätzlichen Mitgliedern synthetisiert.

Motivation

Die Fähigkeit einer Klasse oder Struktur in C#, über mehr als einen Konstruktor zu verfügen, bietet Allgemeinheit, aber auf Kosten eines gewissen Aufwands in der Deklarationssyntax, da die Konstruktoreingaben und der Klassenzustand sauber getrennt werden müssen.

Primäre Konstruktoren legen die Parameter eines Konstruktors in den Bereich der gesamten Klasse oder des Structs, um sie für die Initialisierung oder direkt als Objektstatus zu verwenden. Der Kompromiss besteht darin, dass alle anderen Konstruktoren den primären Konstruktor aufrufen müssen.

public class B(bool b) { } // base class

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(S));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

Detailentwurf

Hier wird das allgemeine Design für Records und Nicht-Records beschrieben. Anschließend wird erläutert, wie die vorhandenen primären Konstruktoren für Records durch Hinzufügen eines Sets von synthetisierten Mitgliedern bei Vorhandensein eines primären Konstruktors spezifiziert werden.

Syntax

Klassen- und Strukturdeklarationen werden erweitert, um eine Parameterliste für den Typnamen, eine Argumentliste auf der Basisklasse und einen Textkörper zuzulassen, der aus nur einem ;besteht:

class_declaration
  : attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
  parameter_list? class_base? type_parameter_constraints_clause* class_body
  ;
  
class_designator
  : 'record' 'class'?
  | 'class'
  
class_base
  : ':' class_type argument_list?
  | ':' interface_type_list
  | ':' class_type  argument_list? ',' interface_type_list
  ;  
  
class_body
  : '{' class_member_declaration* '}' ';'?
  | ';'
  ;
  
struct_declaration
  : attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
    parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
  ;

struct_body
  : '{' struct_member_declaration* '}' ';'?
  | ';'
  ;
  
interface_declaration
  : attributes? interface_modifier* 'partial'? 'interface'
    identifier variant_type_parameter_list? interface_base?
    type_parameter_constraints_clause* interface_body
  ;  
    
interface_body
  : '{' interface_member_declaration* '}' ';'?
  | ';'
  ;

enum_declaration
  : attributes? enum_modifier* 'enum' identifier enum_base? enum_body
  ;

enum_body
  : '{' enum_member_declarations? '}' ';'?
  | '{' enum_member_declarations ',' '}' ';'?
  | ';'
  ;

Anmerkung: Diese Produktionen ersetzen record_declaration in Records und record_struct_declaration in Record-Structs, die beide veraltet sind.

Es ist ein Fehler, wenn ein class_base ein argument_list hat, wenn das einschließende class_declaration nicht ein parameter_list enthält. Maximal eine Partial-Typdeklaration einer partiellen Klasse oder Struct darf ein parameter_list enthalten. Die Parameter in der parameter_list einer record-Deklaration müssen alle Wertparameter sein.

Beachten Sie, dass gemäß diesem Vorschlag class_body, struct_body, interface_body und enum_body dürfen nur aus einer ;bestehen.

Eine Klasse oder Struktur mit einem parameter_list verfügt über einen impliziten öffentlichen Konstruktor, dessen Signatur den Wertparametern der Typdeklaration entspricht. Dies wird als primärer Konstruktor für den Typ bezeichnet und bewirkt, dass der implizit deklarierte parameterlose Konstruktor( falls vorhanden) unterdrückt wird. Es ist ein Fehler, dass ein primärer Konstruktor und ein Konstruktor mit derselben Signatur vorhanden sind, die bereits in der Typdeklaration vorhanden ist.

Suche

Das Lookup von einfachen Namen wurde erweitert, um primäre Konstruktorparameter zu behandeln. Die Änderungen sind im folgenden Auszug in fett hervorgehoben:

  • Andernfalls gilt für jeden Instanztyp T (§15.3.2), beginnend mit dem Instanztyp der unmittelbar eingeschlossenen Typdeklaration und dem Instanztyp jeder eingeschlossenen Klasse oder Struct-Deklaration (falls vorhanden):
    • Wenn die Deklaration von T einen primären Konstruktorparameter I enthält und der Verweis innerhalb der argument_list von T's class_base oder innerhalb eines Initialisierers eines Feldes, einer Eigenschaft oder eines Ereignisses von Terfolgt ist, dann ist das Ergebnis der primäre Konstruktorparameter I.
    • Andernfalls, wenn e null ist und die Deklaration von T einen Typparameter mit dem Namen Ienthält, dann bezieht sich die simple_name auf diesen Typparameter.
    • Andernfalls, wenn eine Membersuche (§12.5) von I in T mit e-Typargumenten eine Übereinstimmung ergibt:
      • Wenn T der Instanztyp der unmittelbar eingeschlossenen Klasse oder Struktur ist und die Suche eine oder mehrere Methoden identifiziert, ist das Ergebnis eine Methodengruppe mit einem zugeordneten Instanzausdruck von this. Wenn eine Typargumentliste angegeben wurde, wird sie beim Aufrufen einer generischen Methode (§12.8.10.2) verwendet.
      • Andernfalls, wenn T der Instanztyp der unmittelbar umschließenden Klasse oder des Strukturtyps ist, wenn die Suche ein Instanzelement identifiziert und der Verweis innerhalb des Blocks eines Instanzkonstruktors, einer Instanzmethode oder eines Instanzaccessors (§12.2.1) auftritt, ist das Ergebnis dasselbe wie bei einem Memberzugriff (§12.8.7) der Form this.I. Dies kann nur passieren, wenn e null ist.
      • Andernfalls entspricht das Ergebnis einem Mitgliedszugriff (§12.8.7) des Formulars T.I oder T.I<A₁, ..., Aₑ>.
    • Andernfalls, wenn die Deklaration von T einen primären Konstruktorparameter Ienthält, ist das Ergebnis der primäre Konstruktorparameter I.

Die erste Ergänzung entspricht der durch Primärkonstruktoren bei Datensätzenverursachten Änderung und stellt sicher, dass Primärkonstruktorparameter vor allen entsprechenden Feldern in Initialisierern sowie in Basisklassenargumenten gefunden werden. Sie erweitert diese Regel auch auf statische Initialisierer. Da Datensätze jedoch immer über ein Instanzmemm mit demselben Namen wie der Parameter verfügen, kann die Erweiterung nur zu einer Änderung in einer Fehlermeldung führen. Unzulässiger Zugriff auf einen Parameter im Vergleich zu unzulässigem Zugriff auf ein Instanzmitglied.

Der zweite Zusatz lässt zu, dass primäre Konstruktorparameter an anderer Stelle im Body des Typs gefunden werden können, aber nur, wenn sie nicht von Mitgliedern überlagert werden.

Es ist ein Fehler, auf einen primären Konstruktorparameter zu verweisen, wenn der Verweis nicht in einem der folgenden Fälle vorkommt:

  • ein nameof Argument
  • ein Initialisierer eines Instanzfelds, einer Eigenschaft oder eines Ereignisses des deklarierenden Typs (Typ, der den primären Konstruktor mit dem Parameter deklariert).
  • das argument_list von class_base des deklarierenden Typs.
  • der Body einer Instanzmethode (beachten Sie, dass Instanz-Konstruktoren ausgeschlossen sind) des deklarierenden Typs.
  • der Body eines Instanz-Accessors des deklarierenden Typs.

Mit anderen Worten, primäre Konstruktorparameter befinden sich im gesamten Bereich des Bodys des deklarierenden Typs. Sie überlagern Mitglieder des deklarierenden Typs innerhalb eines Initialisierers eines Feldes, einer Eigenschaft oder eines Ereignisses des deklarierenden Typs oder innerhalb der argument_list von class_base des deklarierenden Typs. Überall sonst werden sie von Mitgliedern des deklarierenden Typs überlagert.

In der folgenden Deklaration:

class C(int i)
{
    protected int i = i; // references parameter
    public int I => i; // references field
}

Der Initialisierer für das Feld i verweist auf den Parameter i, während der Textkörper der Eigenschaft I auf das Feld iverweist.

Warnung bei Überlagerung durch ein Mitglied von base

Compiler erzeugt eine Warnung bei der Verwendung eines Bezeichners, wenn ein Basismitglied einen primären Konstruktorparameter überschattet, der nicht über seinen Konstruktor an den Basistyp übergeben wurde.

Ein primärer Konstruktorparameter wird als an den Basistyp über seinen Konstruktor übergeben angesehen, wenn für ein Argument in class_basealle folgenden Bedingungen erfüllt sind:

  • Das Argument stellt eine implizite oder explizite Identitätskonvertierung eines primären Konstruktorparameters dar;
  • Das Argument ist nicht Teil eines erweiterten params Arguments;

Semantik

Ein primärer Konstruktor führt zur Generierung eines Instanzkonstruktors für den eingeschlossenen Typ mit den angegebenen Parametern. Wenn die class_base über eine Argumentliste verfügt, verfügt der generierte Instanzkonstruktor über einen base Initialisierer mit derselben Argumentliste.

Primäre Konstruktorparameter in Klassen-/Strukturdeklarationen können ref, in oder outdeklariert werden. Das Deklarieren von ref- oder out-Parametern bleibt in den primären Konstruktoren einer Aufzeichnungsdeklaration unzulässig.

Alle Initialisierungen von Instanzmitgliedern im Body der Klasse werden zu Zuweisungen im generierten Konstruktor.

Wenn ein primärer Konstruktorparameter von einem Instanzmememm aus referenziert wird und sich der Verweis nicht innerhalb eines nameof Arguments befindet, wird er im Zustand des eingeschlossenen Typs erfasst, sodass er nach dem Beenden des Konstruktors zugänglich bleibt. Eine wahrscheinliche Strategie für die Implementierung ist ein privates Feld mit einem verfälschten Namen. In einem readonly Struct sind die Capture-Felder readonly. Daher hat der Zugriff auf erfasste Parameter einer readonly-Struktur ähnliche Einschränkungen wie der Zugriff auf readonly-Felder. Der Zugriff auf erfasste Parameter innerhalb eines readonly-Elements hat ähnliche Einschränkungen wie der Zugriff auf Instanzfelder im selben Kontext.

Capturing ist nicht zulässig für Parameter, die einen ref-ähnlichen Typ haben, und Capturing ist nicht zulässig für ref, in oder out Parameter. Dies ähnelt einer Einschränkung für die Erfassung in Lambdas.

Wenn auf einen primären Konstruktorparameter nur innerhalb von Instanzmember-Initialisierern verwiesen wird, können diese direkt auf den Parameter des generierten Konstruktors verweisen, da sie als Teil davon ausgeführt werden.

Der primäre Konstruktor führt die folgende Abfolge von Vorgängen aus:

  1. Parameterwerte werden ggf. in Aufnahmefeldern gespeichert.
  2. Instanz-Initialisierer werden ausgeführt
  3. Der Initialisierer des Basiskonstruktors wird aufgerufen

Parameterverweise in einem beliebigen Benutzercode werden durch entsprechende Erfassungsfeldverweise ersetzt.

Beispielsweise diese Deklaration:

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

Generiert Code ähnlich wie folgt:

public class C : B
{
    public int I { get; set; }
    public string S
    {
        get => __s;
        set => __s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(0, s) { ... } // must call this(...)
    
    // generated members
    private string __s; // for capture of s
    public C(bool b, int i, string s)
    {
        __s = s; // capture s
        I = i; // run I's initializer
        B(b) // run B's constructor
    }
}

Es ist ein Fehler, wenn eine nicht-primäre Konstruktordeklaration dieselbe Parameterliste wie der primäre Konstruktor hat. Alle nicht primären Konstruktordeklarationen müssen einen this Initialisierer verwenden, sodass der primäre Konstruktor letztendlich aufgerufen wird.

Records erzeugen eine Warnung, wenn ein primärer Konstruktorparameter nicht innerhalb der (möglicherweise generierten) Instanz-Initialisierer oder des Basis-Initialisierers gelesen wird. Ähnliche Warnungen werden für primäre Konstruktorparameter in Klassen und Strukturen gemeldet:

  • für einen By-Value-Parameter, wenn der Parameter nicht erfasst wird und nicht innerhalb von Instanz-Initialisierern oder Basis-Initialisierern gelesen wird.
  • für einen in-Parameter, wenn der Parameter nicht in einem Instanz-Initialisierer oder Basis-Initialisierer gelesen wird.
  • für einen ref-Parameter, wenn der Parameter nicht innerhalb eines Instanz-Initialisierers oder Basis-Initialisierers gelesen oder beschrieben wird.

Identische einfache Namen und Typnamen

Es gibt eine spezielle Sprachregel für Szenarien, die oft als "Color Color" Szenarien bezeichnet werden – Identische einfache Namen und Typnamen.

In einem Memberzugriff der Form E.I, wenn E ein einzelner Bezeichner ist, und wenn die Bedeutung von E als simple_name (§12.8.4) eine Konstante, ein Feld, eine Eigenschaft, eine lokale Variable oder ein Parameter mit demselben Typ als die Bedeutung von E als type_name (§7.8.1) ist, dann sind beide möglichen Bedeutungen von E zulässig. Die Membersuche von E.I ist nie zweideutig, da I in beiden Fällen zwingend ein Mitglied des Typs E sein muss. Mit anderen Worten, die Regel erlaubt einfach den Zugriff auf die statischen Member und geschachtelten Typen von E, bei denen andernfalls ein Kompilierungsfehler aufgetreten wäre.

Im Hinblick auf primäre Konstruktoren wirkt sich die Regel darauf aus, ob ein Bezeichner innerhalb eines Instanzmitglieds als Typverweis behandelt werden soll, oder als primärer Konstruktorparameterverweis, der wiederum den Parameter in den Zustand des eingeschlossenen Typs erfasst. Auch wenn "die der Mitglieder-Lookup von E.I niemals mehrdeutig ist", ist es in einigen Fällen, wenn die Suche eine Mitgliedergruppe ergibt, unmöglich festzustellen, ob sich ein Mitgliedszugriff auf ein statisches Mitglied oder ein Instanzmitglied bezieht, ohne den Mitgliedszugriff vollständig aufzulösen (zu binden). Gleichzeitig ändert das Erfassen eines primären Konstruktorparameters eigenschaften des eingeschlossenen Typs auf eine Weise, die sich auf die semantische Analyse auswirkt. Beispielsweise wird der Typ möglicherweise nicht verwaltet und kann daher bestimmte Einschränkungen nicht erfüllen. Es gibt sogar Szenarien, in denen die Bindung entweder erfolgreich sein kann, je nachdem, ob der Parameter als erfasst betrachtet wird oder nicht. Zum Beispiel:

struct S1(Color Color)
{
    public void Test()
    {
        Color.M1(this); // Error: ambiguity between parameter and typename
    }
}

class Color
{
    public void M1<T>(T x, int y = 0)
    {
        System.Console.WriteLine("instance");
    }
    
    public static void M1<T>(T x) where T : unmanaged
    {
        System.Console.WriteLine("static");
    }
}

Wenn der Empfänger Color als Wert behandelt wird, erfassen wir den Parameter und "S1" wird verwaltet. Dann wird die statische Methode aufgrund der Einschränkung nicht mehr angewendet, und wir würden die Instanzmethode aufrufen. Wenn wir den Empfänger jedoch als Typ behandeln, erfassen wir den Parameter nicht, und 'S1' bleibt nicht verwaltet, dann sind beide Methoden anwendbar, aber die statische Methode ist "besser", da sie keinen optionalen Parameter aufweist. Keine der beiden Auswahlmöglichkeiten führt zu einem Fehler, aber jedes würde zu einem unterschiedlichen Verhalten führen.

In diesem Fall erzeugt der Compiler einen Mehrdeutigkeitsfehler für einen Mitgliedszugriff E.I, wenn alle folgenden Bedingungen erfüllt sind:

  • Die Mitgliedersuche von E.I ergibt eine Mitgliedergruppe, die sowohl Instanzen als auch statische Mitglieder enthält. Erweiterungsmethoden, die für den Empfängertyp gelten, werden als Instanzmethoden für diese Überprüfung behandelt.
  • Wenn E anstelle eines Typnamens als einfacher Name behandelt wird, würde er auf einen primären Konstruktorparameter verweisen und den Parameter in den Zustand des eingeschlossenen Typs aufnehmen.

Warnungen vor doppeltem Storage

Wenn ein primärer Konstruktorparameter an die Basis übergeben wird und auch erfasst werden, besteht ein hohes Risiko, dass er versehentlich zweimal im Objekt gespeichert wird.

Der Compiler erzeugt eine Warnung für in oder ein Wertargument in einem class_baseargument_list, wenn alle folgenden Bedingungen erfüllt sind:

  • Das Argument stellt eine implizite oder explizite Identitätskonvertierung eines primären Konstruktorparameters dar;
  • Das Argument ist nicht Teil eines erweiterten params Arguments;
  • Der primäre Konstruktorparameter wird in den Status des umschließenden Typs aufgenommen.

Der Compiler erzeugt eine Warnung für eine variable_initializer, wenn alle folgenden Bedingungen erfüllt sind:

  • Der Variable initializer stellt eine implizite oder explizite Identitätskonvertierung eines primären Konstruktorparameters dar;
  • Der primäre Konstruktorparameter wird in den Status des umschließenden Typs aufgenommen.

Zum Beispiel:

public class Person(string name)
{
    public string Name { get; set; } = name;   // warning: initialization
    public override string ToString() => name; // capture
}

Attribute, die auf primäre Konstruktoren abzielen

Bei https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md haben wir beschlossen, den https://github.com/dotnet/csharplang/issues/7047 Vorschlag anzunehmen.

Das Ziel des Attributs "method" ist auf einer class_declaration/struct_declaration mit parameter_list zulässig und führt dazu, dass der entsprechende primäre Konstruktor dieses Attribut hat. Attribute mit dem Ziel method auf einer Klassen_deklaration/Konstrukt_deklaration ohne Parameter_liste werden mit einer Warnung ignoriert.

[method: FooAttr] // Good
public partial record Rec(
    [property: Foo] int X,
    [field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
    public void Frobnicate()
    {
        ...
    }
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;

Primäre Konstruktoren für Datensätze

Mit diesem Vorschlag müssen Datensätze keinen primären Konstruktormechanismus mehr separat angeben. Stattdessen entsprechen Datensatzdeklarationen (Klassen- und Strukturdeklarationen), die primäre Konstruktoren haben, den allgemeinen Regeln, mit den folgenden einfachen Ergänzungen:

  • Wenn für jeden primären Konstruktorparameter bereits ein Mitglied mit demselben Namen existiert, muss es sich um eine Instanz-Eigenschaft oder ein Feld handeln. Ist dies nicht der Fall, wird eine öffentliche init-only Auto-Eigenschaft gleichen Namens mit einem Eigenschaftsinitialisierer synthetisiert, der von dem Parameter aus zuweist.
  • Ein Dekonstruktor wird mit Parametern synthetisiert, die mit den primären Konstruktorparametern übereinstimmen.
  • Wenn es sich bei einer expliziten Konstruktordeklaration um einen "Kopierkonstruktor" handelt – einen Konstruktor, der einen einzelnen Parameter des eingeschlossenen Typs verwendet –, ist es nicht erforderlich, einen this-Initialisierer aufzurufen, und die Member-Initialisierer, die in der Datensatzdeklaration angegeben sind, werden nicht ausgeführt.

Nachteile

  • Die Zuordnungsgröße von konstruierten Objekten ist weniger offensichtlich, da der Compiler bestimmt, ob ein Feld für einen primären Konstruktorparameter basierend auf dem vollständigen Text der Klasse zugewiesen werden soll. Dieses Risiko ähnelt der impliziten Erfassung von Variablen durch Lambda-Ausdrücke.
  • Eine häufige Versuchung (oder ein versehentliches Muster) könnte darin bestehen, den "gleichen" Parameter auf mehreren Vererbungsebenen zu erfassen, während er durch die Konstruktorkette weitergegeben wird, anstatt ihm ein geschütztes Feld in der Basisklasse explizit zuzuweisen, was zu mehrfachen Zuweisungen derselben Daten in Objekten führt. Dies ähnelt dem heutigen Risiko, Auto-Properties mit Auto-Properties zu überschreiben.
  • Wie hier vorgeschlagen, gibt es keinen Ort für zusätzliche Logik, die in der Regel in Konstruktorgremien ausgedrückt werden kann. Die untenstehende Erweiterung "primäre Konstruktor Bodies" geht darauf ein.
  • In der vorgeschlagenen Form unterscheidet sich die Semantik der Ausführungsreihenfolge geringfügig von der in gewöhnlichen Konstruktoren, da die Initialisierung von Mitgliedern auf die Zeit nach den Basisaufrufen verschoben wird. Dies könnte wahrscheinlich korrigiert werden, allerdings auf Kosten einiger Erweiterungsvorschläge (insbesondere der "primären Konstruktor Bodies").
  • Der Vorschlag funktioniert nur für Szenarien, in denen ein einzelner Konstruktor primär festgelegt werden kann.
  • Es gibt keine Möglichkeit, die getrennte Zugänglichkeit der Klasse und des primären Konstruktors zu definieren. Ein Beispiel ist, wenn alle öffentlichen Konstruktoren an einen privaten "Build-it-all"-Konstruktor delegieren. Falls erforderlich, könnte die Syntax später vorgeschlagen werden.

Alternativen

Keine Erfassung

Eine viel einfachere Version der Funktion würde es verbieten, dass primäre Konstruktorparameter in Member Bodies vorkommen. Das Verweisen auf sie wäre ein Fehler. Felder müssen explizit deklariert werden, wenn Speicher über den Initialisierungscode hinaus gewünscht wird.

public class C(string s)
{
    public string S1 => s; // Nope!
    public string S2 { get; } = s; // Still allowed
}

Dies könnte zu einem späteren Zeitpunkt immer noch zum vollständigen Vorschlag weiterentwickelt werden und würde eine Reihe von Entscheidungen und Komplexitäten vermeiden, allerdings um den Preis, dass anfangs weniger Textbausteine entfernt werden müssten und es wahrscheinlich auch nicht intuitiv erscheint.

Explizit generierte Felder

Ein alternativer Ansatz ist, dass primäre Konstruktorparameter immer und sichtbar ein Feld mit demselben Namen generieren. Anstatt die Parameter wie bei lokalen und anonymen Funktionen zu schließen, würde es explizit eine generierte Mitglied-Deklaration geben, ähnlich wie bei den öffentlichen Eigenschaften, die für primäre Konstruktionsparameter in Records generiert werden. Genau wie bei Datensätzen würde, wenn bereits ein geeignetes Mitglied vorhanden ist, kein weiteres Mitglied generiert.

Wenn das generierte Feld privat ist, könnte es immer noch weggelassen werden, wenn es nicht als Feld in Member Bodies verwendet wird. In Klassen wäre ein privates Feld jedoch oft nicht die richtige Wahl, da es in abgeleiteten Klassen zu einer Verdoppelung des Status führen könnte. Eine Option wäre hier, stattdessen ein geschütztes Feld in Klassen zu generieren und die Wiederverwendung von Speicher über Vererbungsebenen hinweg zu fördern. Dann wären wir jedoch nicht in der Lage, die Deklaration zu elidieren, und für jeden primären Konstruktorparameter würden Zuordnungskosten anfallen.

Dies würde die primären Konstruktoren ohne Records stärker an die Konstruktoren mit Records angleichen, da die Mitglieder immer (zumindest konzeptionell) generiert werden, wenn auch unterschiedliche Arten von Mitgliedern mit unterschiedlichen Zugriffsfreiheiten. Es würde jedoch auch zu überraschenden Unterschieden in der Art und Weise führen, wie Parameter und lokale Variablen an anderer Stelle in C# erfasst werden. Wenn wir beispielsweise lokale Klassen zulassen würden, würden sie die umschließenden Parameter und Lokale automatisch einfangen. Das sichtbare Generieren von Schattierungsfeldern für sie scheint kein vernünftiges Verhalten zu sein.

Ein weiteres Problem, das häufig mit diesem Ansatz ausgelöst wird, besteht darin, dass viele Entwickler unterschiedliche Benennungskonventionen für Parameter und Felder haben. Welches sollte für den primären Konstruktorparameter verwendet werden? Beide Optionen würden zu Inkonsistenzen mit dem rest des Codes führen.

Schließlich ist das sichtbare Generieren von Mitglieder-Deklarationen bei Records das A und O, bei Nicht-Record-Klassen und Structs jedoch viel überraschender und "untypischer". Alles in allem sind dies die Gründe, warum der Hauptvorschlag für eine implizite Erfassung optiert, mit einem vernünftigen Verhalten (konsistent mit Records) für explizite Mitgliederdeklarationen, wenn diese gewünscht sind.

Instanzmitglieder aus dem Bereich der Initialisierung entfernen

Die oben genannten Nachschlageregeln sollen das bestehende Verhalten von primären Konstruktorparametern in Datensätzen ermöglichen, wenn ein entsprechendes Mitglied manuell deklariert wird, und das Verhalten des generierten Mitglieds erläutern, wenn dies nicht der Fall ist. Dies erfordert eine Unterscheidung des Lookups zwischen dem "Initialisierungsbereich" (This/Base-Initialisierer, Member-Initialisierer) und dem "Body-Bereich" (Member-Bodies), was der obige Vorschlag dadurch erreicht, dass wann primäre Konstruktorparameter gesucht werden, je nachdem, wo die Referenz auftritt.

Eine Beobachtung ist, dass der Verweis auf ein Instanzmitglied mit einem einfachen Namen im Bereich der Initialisierer immer zu einem Fehler führt. Anstatt Instanzmitglieder an diesen Stellen zu überlagern, könnten wir sie einfach aus dem Bereich nehmen? Auf diese Weise wäre diese seltsame bedingte Anordnung von Bereichen nicht vorhanden.

Diese Alternative ist wahrscheinlich möglich, aber es hätte einige Folgen, die etwas weit reichend und potenziell unerwünscht sind. Zunächst einmal, wenn wir Instanzmitglieder aus dem Bereich des Initialisierers entfernen, dann könnte ein einfacher Name, der einem Instanzmitglied und nicht einem primären Konstruktorparameter entspricht, versehentlich an etwas außerhalb der Typdeklaration binden! Dies scheint so zu sein, als wäre es selten beabsichtigt, und ein Fehler wäre besser.

Außerdem ist es in Ordnung, statische Mitglieder im Initialisierungsbereich zu referenzieren. Daher müssten wir bei der Suche zwischen statischen und Instanzmitgliedern unterscheiden, was wir heute nicht tun. (Wir unterscheiden zwar bei der Überladungsauflösung, aber das ist hier nicht von Belang). Das müsste also auch geändert werden, was zu noch mehr Situationen führen würde, in denen z. B. in statischen Kontexten etwas "weiter hinaus" binden würde, anstatt einen Fehler zu machen, weil es ein Instanzmitglied gefunden hat.

All diese "Vereinfachung" würde zu einer ziemlich nachgelagerten Komplikation führen, die niemand verlangt hat.

Mögliche Erweiterungen

Dies sind Variationen oder Ergänzungen des Kernvorschlags, die in Verbindung mit dem Vorschlag oder zu einem späteren Zeitpunkt als nützlich betrachtet werden können.

Primärer Konstruktorparameterzugriff in Konstruktoren

Die oben genannten Regeln führen dazu, dass es ein Fehler ist, auf einen primären Konstruktorparameter innerhalb eines anderen Konstruktors zu verweisen. Dies könnte jedoch innerhalb des Bodys anderer Konstruktoren zugelassen werden, da der primäre Konstruktor zuerst ausgeführt wird. Jedoch müsste es in der Argumentliste des this-Initialisierers weiterhin unzulässig bleiben.

public class C(bool b, int i, string s) : B(b)
{
    public C(string s) : this(b, s) // b still disallowed
    { 
        i++; // could be allowed
    }
}

Ein solcher Zugriff würde immer noch zu einem Capture führen, da dies die einzige Möglichkeit für den Body des Konstruktors wäre, an die Variable zu gelangen, nachdem der primäre Konstruktor bereits ausgeführt wurde.

Das Verbot primärer Konstruktorparameter in den Argumenten dieses Initialisierers könnte geschwächt werden, um sie zuzulassen, sie aber nicht definitiv zugewiesen zu machen, aber das scheint nicht nützlich zu sein.

Konstruktoren ohne this Initialisierer zulassen

Konstruktoren ohne this Initialisierer (d.h. mit einem impliziten oder expliziten base Initialisierer) könnten zugelassen werden. Ein solcher Konstruktor würde nicht Instanzfeld, Eigenschafts- und Ereignisinitialisierer ausführen, da diese nur als Teil des primären Konstruktors betrachtet werden.

In Anwesenheit solcher Basisaufrufkonstruktoren gibt es einige Optionen für die Behandlung der primären Konstruktorparametererfassung. Am einfachsten ist es, die Erfassung in dieser Situation vollständig zu verbieten. Primäre Konstruktorparameter würden nur dann initialisiert werden, wenn solche Konstruktoren vorhanden sind.

In Kombination mit der zuvor beschriebenen Möglichkeit, den Zugriff auf primäre Konstruktorparameter innerhalb von Konstruktoren zuzulassen, könnten die Parameter in den Body des Konstruktors als nicht definitiv zugewiesen eintreten, und diejenigen, die erfasst werden, müssten am Ende des Bodys des Konstruktors definitiv zugewiesen werden. Sie wären dann im Wesentlichen implizite Out-Parameter. Auf diese Weise würden erfasste primäre Konstruktorparameter immer einen sinnvollen (d. h. explizit zugewiesenen) Wert aufweisen, wenn sie von anderen Funktionsmitgliedern verbraucht werden.

Ein Vorteil dieser Erweiterung (in beiden Formen) ist, dass sie die aktuelle Ausnahme für "Kopierkonstruktoren" in Datensätzen vollständig verallgemeinert, ohne dass es zu Situationen kommt, in denen nicht initialisierte primäre Konstruktorparameter beobachtet werden. Im Wesentlichen sind Konstruktoren, die das Objekt auf alternative Weise initialisieren, in Ordnung. Die erfassungsbezogenen Einschränkungen wären keine wesentliche Änderung für vorhandene manuell definierte Kopierkonstruktoren in Datensätzen, da Datensätze niemals ihre primären Konstruktorparameter erfassen (sie generieren stattdessen Felder).

public class C(bool b, int i, string s) : B(b)
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s2) : base(true) // cannot use `string s` because it would shadow
    { 
        s = s2; // must initialize s because it is captured by S
    }
    protected C(C original) : base(original) // copy constructor
    {
        this.s = original.s; // assignment to b and i not required because not captured
    }
}

Primäre Konstruktor-Bodys

Konstruktoren selbst enthalten häufig Parameterüberprüfungslogik oder anderen nichttriviellen Initialisierungscode, der nicht als Initialisierer ausgedrückt werden kann.

Primäre Konstruktoren können erweitert werden, damit Anweisungsblöcke direkt im Klassentext angezeigt werden können. Diese Anweisungen würden in den generierten Konstruktor an der Stelle eingefügt, an der sie zwischen initialisierenden Zuweisungen auftauchen, und würden somit zwischen den Initialisierungen ausgeführt. Zum Beispiel:

public class C(int i, string s) : B(s)
{
    {
        if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
    }
	int[] a = new int[i];
    public int S => s;
}

Ein Großteil dieses Szenarios könnte angemessen abgedeckt werden, wenn wir "finale Initialisierer" einführen würden, die nach der Vervollständigung der Konstruktoren und aller Objekt-/ Collection-Initialisierer ausgeführt werden. Die Argumentüberprüfung ist jedoch eine Sache, die idealerweise so früh wie möglich geschehen würde.

Primäre Konstruktor-Bodys könnten auch einen Platz bieten, um einen Zugriffsmodifikator für den primären Konstruktor zuzulassen, der ihm die Möglichkeit gibt, von der Zugriffsfreiheit des umschließenden Typs abzuweichen.

Kombinierte Parameter- und Mitgliederdeklarationen

Eine mögliche und oft erwähnte Ergänzung könnte sein, die Möglichkeit zu bieten, primäre Konstruktorparameter mit Anmerkungen zu versehen, so dass sie auch ein Mitglied des Typs deklarieren. Am häufigsten wird vorgeschlagen, einen Zugriffsbezeichner für die Parameter zuzulassen, um die Elementgenerierung auszulösen:

public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
    void M()
    {
        ... i ... // refers to the field i
        ... s ... // closes over the parameter s
    }
}

Es gibt einige Probleme:

  • Was ist, wenn eine Eigenschaft und kein Feld gewünscht ist? Eine { get; set; }-Syntax inline in einer Parameterliste zu haben, scheint nicht sehr appetitlich.
  • Was geschieht, wenn für Parameter und Felder unterschiedliche Benennungskonventionen verwendet werden? Dieses Feature wäre dann nutzlos.

Dies ist eine mögliche zukünftige Ergänzung, die angenommen werden kann oder nicht. Der aktuelle Vorschlag lässt die Möglichkeit offen.

Offene Fragen

Lookup-Reihenfolge für Typparameter

Der Abschnitt https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup gibt an, dass Typparameter des deklarierenden Typs in jedem Kontext, in dem sich diese Parameter im Bereich befinden, vor den primären Konstruktorparametern des Typs kommen sollen. Wir haben jedoch bereits vorhandenes Verhalten mit Datensätzen – primäre Konstruktorparameter kommen vor Typparametern in Basisinitialisierungs- und Feldinitialisierern.

Was sollten wir mit dieser Diskrepanz tun?

  • Passen Sie die Regeln so an, dass sie dem Verhalten entsprechen.
  • Passen Sie das Verhalten an (eine mögliche kompatibilitätsbrechende Änderung).
  • Verbieten Sie, dass ein Primiry-Konstruktor-Parameter den Namen des Typparameters verwendet (eine möglicherweise fehlerhafte Änderung).
  • Nehmen Sie nichts vor, akzeptieren Sie die Inkonsistenz zwischen der Spezifikation und Implementierung.

Schlussfolgerung:

Passen Sie die Regeln so an, dass sie dem Verhalten entsprechen (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors).

Feldadressierungsattribute für erfasste primäre Konstruktorparameter

Sollen Feldadressierungsattribute für erfasste primäre Konstruktorparameter zugelassen werden?

class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
    public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = x;
}

Zurzeit werden die Attribute unabhängig davon, ob der Parameter erfasst wird, trotz Warnung ignoriert.

Beachten Sie, dass für Datensätze feldorientierte Attribute zulässig sind, wenn eine Eigenschaft dafür synthetisiert wird. Die Attribute werden dann auf das Backing-Feld übertragen.

record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = X;
}

Schlussfolgerung:

Nicht zulässig (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters).

Warnung bei Überlagerung durch ein Mitglied von base

Sollten wir eine Warnung melden, wenn ein Element von der Basis einen primären Konstruktorparameter innerhalb eines Elements schattiert (siehe https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621)?

Schlussfolgerung:

Ein alternatives Design ist genehmigt - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors

Erfassen der Instanz des umschließenden Typs in einer Closure

Wenn ein Parameter, der im Zustand des eingeschlossenen Typs erfasst wird, auch in einer Lambda-Funktion innerhalb eines Instanzinitialisierers oder eines Basisinitialisierers referenziert wird, sollten die Lambda-Funktion und der Zustand des eingeschlossenen Typs auf denselben Ort für den Parameter verweisen. Zum Beispiel:

partial class C1
{
    public System.Func<int> F1 = Execute1(() => p1++);
}

partial class C1 (int p1)
{
    public int M1() { return p1++; }
    static System.Func<int> Execute1(System.Func<int> f)
    {
        _ = f();
        return f;
    }
}

Da die naive Implementierung der Erfassung eines Parameters im Status des Typs den Parameter einfach in einem privaten Instanzfeld erfasst, muss das Lambda auf dasselbe Feld verweisen. Daher muss sie auf die Instanz des Typs zugreifen können. Dies erfordert die Erfassung von this in einer Closure, bevor der Basiskonstruktor aufgerufen wird. Das wiederum führt zu einer sicheren, aber nicht überprüfbaren AWL. Ist dies akzeptabel?

Alternativ können wir:

  • Verbieten Sie solche Lambdas;
  • Oder erfassen Sie stattdessen Parameter wie diese in einer Instanz einer separaten Klasse (noch eine weitere Schließung), und teilen Sie diese Instanz zwischen dem Schließen und der Instanz des eingeschlossenen Typs. Damit entfällt die Anforderung, this in einer Closure zu erfassen.

Schlussfolgerung:

Wir haben kein Problem damit, this in eine Schließung zu erfassen, ehe der Basiskonstruktor aufgerufen wird (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md). Das Runtime Team fand das IL-Muster ebenfalls nicht problematisch.

Zuweisung an this innerhalb eines Structs

C# bietet die Möglichkeit, this innerhalb einer Struct zuzuweisen. Wenn die Struktur einen primären Konstruktorparameter erfasst, überschreibt die Zuordnung den Wert, der für den Benutzer möglicherweise nicht offensichtlich ist. Sollen wir für Zuweisungen wie diese eine Warnung liefern?

struct S(int x)
{
    int X => x;
    
    void M(S s)
    {
        this = s; // 'x' is overwritten
    }
}

Schlussfolgerung:

Zulässig, keine Warnung (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md).

Doppelte Speicherwarnung für Initialisierung und Erfassung

Wir haben eine Warnung, wenn ein primärer Konstruktorparameter an die Basis übergeben wird, und auch erfasst, da ein hohes Risiko besteht, dass er versehentlich zweimal im Objekt gespeichert wird.

Es scheint, dass ein ähnliches Risiko besteht, wenn ein Parameter zum Initialisieren eines Elements verwendet wird und auch erfasst wird. Hier ist ein kleines Beispiel:

public class Person(string name)
{
    public string Name { get; set; } = name;   // initialization
    public override string ToString() => name; // capture
}

Bei einer gegebenen Instanz von Person würden sich Änderungen an Name nicht in der Ausgabe von ToString widerspiegeln, was vom Entwickler wahrscheinlich nicht beabsichtigt ist.

Sollten wir eine doppelte Speicherwarnung für diese Situation einführen?

So funktioniert es:

Der Compiler erzeugt eine Warnung für eine variable_initializer, wenn alle folgenden Bedingungen erfüllt sind:

  • Der Variable initializer stellt eine implizite oder explizite Identitätskonvertierung eines primären Konstruktorparameters dar;
  • Der primäre Konstruktorparameter wird in den Status des umschließenden Typs aufgenommen.

Schlussfolgerung:

Genehmigt, siehe https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors

LDM-Treffen