Primäre Konstrukteure
Wenn du einen Datensatz mit einer Parameterliste definierst, erzeugt der Compiler automatisch Eigenschaftsdeklarationen sowie einen primären Konstruktor (und einen Dekonstruktor). Wie wir gesehen haben, funktioniert das in einfachen Fällen gut. In komplexeren Fällen kannst du die Parameterliste weglassen und die Eigenschaftsdeklarationen und den Konstruktor manuell schreiben.
C# bietet auch eine halbwegs nützliche Zwischenlösung - wenn du bereit bist, dich mit der merkwürdigen Semantik von primären Konstruktoren auseinanderzusetzen - nämlich eine Parameterliste zu definieren und dabei einige oder alle Eigenschaftsdeklarationen selbst zu schreiben:
record Student (string ID, string LastName, string GivenName) { public string ID { get; } = ID; }
In diesem Fall haben wir die Definition der Eigenschaft ID
"übernommen", indem wir sie als schreibgeschützt definiert haben (anstelle von "init-only"), um zu verhindern, dass sie an der zerstörungsfreien Mutation teilnimmt. Wenn du eine bestimmte Eigenschaft nie zerstörungsfrei ändern musst, kannst du die Daten im Datensatz speichern, ohne einen Aktualisierungsmechanismus programmieren zu müssen, indem du sie schreibgeschützt machst.
Beachte, dass wir einen Eigenschaftsinitialisierer (in Fettschrift) einfügen mussten:
public string ID { get; } = ID;
Wenn du eine Eigenschaftsdeklaration "übernimmst", bist du für die Initialisierung ihres Wertes verantwortlich; der primäre Konstruktor tut dies nicht mehr automatisch. (Das entspricht genau dem Verhalten, wenn du primäre Konstruktoren für Klassen oder Strukturen definierst). Beachte auch, dass sich die fett gedruckte ID
auf den Parameter des primären Konstruktors bezieht, nicht auf die Eigenschaft ID
.
Hinweis
Bei Record-Strukturen ist es legal, eine Eigenschaft als Feld umzudefinieren:
record struct Student (string ID) { public string ID = ID; }
In Übereinstimmung mit der Semantik der primären Konstruktoren von Klassen und Strukturen (siehe "Primäre Konstruktoren") sind die Parameter des primären Konstruktors (in diesem FallID
, LastName
und GivenName
) auf magische Weise für alle Feld- und Eigenschaftsinitialisierer sichtbar. Wir können dies veranschaulichen, indem wir unser Beispiel wie folgt erweitern:
record Student (string ID, string LastName, string FirstName) { public string ID { get; } = ID; readonly int _enrollmentYear = int.Parse (ID.Substring (0, 4)); }
Auch hier bezieht sich die fettgedruckte ID
auf den primären Konstruktorparameter, nicht auf die Eigenschaft. (Der Grund dafür, dass es keine Zweideutigkeit gibt, ist, dass es illegal ist, von Initialisierern auf Eigenschaften zuzugreifen).
In diesem Beispiel haben wir _enrollmentYear
aus den ersten vier Ziffern von ID
errechnet. Obwohl es sicher ist, dies in einem schreibgeschützten Feld zu speichern (weil die Eigenschaft ID
schreibgeschützt ist und daher nicht zerstörungsfrei geändert werden kann), würde dieser Code in der realen Welt nicht so gut funktionieren. Der Grund dafür ist, dass es ohne einen expliziten Konstruktor keine zentrale Stelle gibt, an der ID
überprüft werden kann und eine sinnvolle Ausnahme geworfen wird, wenn sie ungültig ist (eine häufige Anforderung).
Die Validierung ist auch ein guter Grund dafür, explizite Init-Only-Accessors zu schreiben (wie wir in "Property Validation" besprochen haben ). Leider spielen primäre Konstruktoren in diesem Szenario keine gute Rolle. Zur Veranschaulichung betrachten wir den folgenden Datensatz, in dem ein init
Accessor eine Validierungsprüfung auf Null durchführt:
record Person (string Name) { string _name = Name; public string Name { get => _name; init => _name = value ?? throw new ArgumentNullException ("Name"); } }
Da Name
keine automatische Eigenschaft ist, kann sie keinen Initialisierer definieren. Das Beste, was wir tun können, ist, den Initialisierer auf das Hintergrundfeld zu setzen (in Fettschrift). Leider wird dadurch die Nullprüfung umgangen:
var p = new Person (null); // Succeeds! (bypasses the null check)
Das Problem ist, dass es keine Möglichkeit gibt, einen primären Konstruktorparameter einer Eigenschaft zuzuweisen, ohne den Konstruktor selbst zu schreiben. Es gibt zwar Umgehungsmöglichkeiten (z. B. die Validierungslogik von init
in einer separaten statischen Methode, die wir zweimal aufrufen), aber die einfachste Lösung besteht darin, die Parameterliste ganz zu vermeiden und einen gewöhnlichen Konstruktor manuell zu schreiben (und einen Dekonstruktor, falls du ihn brauchst):
record Person { public Person (string name) => Name = name; // Assign to *PROPERTY* string _name; public string Name { get => _name; init => ... } }