C# 14: Das field-Schlüsselwort und Eigenschaften mit field-basierter Speicherung
C# 14 führt das kontextuelle Schlüsselwort field für Eigenschafts-Accessoren ein. So können Sie zu Auto-Properties benutzerdefinierte Logik hinzufügen, ohne ein separates Hintergrundfeld zu deklarieren.
C# 14 führt ein neues kontextuelles Schlüsselwort ein, field, das innerhalb der Accessoren einer Eigenschaft (den Blöcken get, set oder init) verwendet werden kann, um auf die zugrunde liegende Speicherung der Eigenschaft zu verweisen. Einfach gesagt ist field ein Platzhalter, der die verborgene Variable repräsentiert, in der der Wert einer Eigenschaft gespeichert wird. Mit diesem Schlüsselwort können Sie automatisch implementierten Eigenschaften benutzerdefinierte Logik hinzufügen, ohne manuell ein separates privates Feld zu deklarieren. Es war erstmals als Vorschau in C# 13 verfügbar (mit .NET 9 und der Sprachversion auf Preview gesetzt) und ist offiziell Teil der Sprache in C# 14.
Warum ist das nützlich? Wollten Sie vor C# 14 Logik (etwa Validierung oder Änderungs-Benachrichtigung) zu einer Eigenschaft hinzufügen, mussten Sie sie zu einer vollständigen Eigenschaft mit privatem Hintergrundfeld machen. Das bedeutete mehr Boilerplate-Code und das Risiko, dass andere Klassenmitglieder versehentlich direkt auf das Feld zugreifen und so die Eigenschaftslogik umgehen. Das neue field-Schlüsselwort löst diese Probleme, indem der Compiler das Hintergrundfeld für Sie erzeugt und verwaltet, während Sie in Ihrem Eigenschaftscode einfach field verwenden. Das Ergebnis sind klarere, wartungsfreundlichere Eigenschaftsdeklarationen, und das Hintergrundspeicher leakt nicht in den restlichen Gültigkeitsbereich Ihrer Klasse.
Vorteile und Anwendungsfälle von field
Das Schlüsselwort field wurde eingeführt, um Eigenschaftsdeklarationen prägnanter und weniger fehleranfällig zu machen. Hier die wichtigsten Vorteile und Szenarien, in denen es nützlich ist:
-
Manuelle Hintergrundfelder entfallen: Sie müssen nicht mehr für jede Eigenschaft ein privates Mitgliedsfeld schreiben, nur um benutzerdefiniertes Verhalten hinzuzufügen. Der Compiler stellt automatisch ein verborgenes Hintergrundfeld bereit, auf das über das Schlüsselwort
fieldzugegriffen wird. Das reduziert Boilerplate-Code und hält Ihre Klassendefinition übersichtlicher. -
Eigenschaftszustand bleibt gekapselt: Das vom Compiler erzeugte Hintergrundfeld ist nur über die Accessoren der Eigenschaft (per
field) zugänglich, nicht woanders in Ihrer Klasse. Das verhindert versehentlichen Missbrauch des Felds aus anderen Methoden oder Eigenschaften und stellt sicher, dass Invarianten oder Validierungen im Eigenschafts-Accessor nicht umgangen werden können. -
Einfachere Eigenschaftslogik (Validierung, Lazy Initialization usw.): Es bietet einen sauberen Weg, Logik zu Auto-Properties hinzuzufügen. Häufige Szenarien sind:
- Validierung oder Bereichsprüfung: z. B. sicherstellen, dass ein Wert nicht negativ ist oder in einem Bereich liegt, bevor er akzeptiert wird.
- Änderungs-Benachrichtigung: z. B.
INotifyPropertyChanged-Ereignisse nach dem Setzen eines neuen Werts auslösen. - Lazy Initialization oder Standardwerte: z. B. im Getter
fieldbeim ersten Zugriff initialisieren oder einen Standardwert zurückgeben, falls noch nicht gesetzt.
In früheren C#-Versionen erforderten diese Szenarien eine vollständige Eigenschaft mit separatem Feld. Mit
fieldkönnen Sie sie direkt in derget/set-Logik der Eigenschaft umsetzen, ohne zusätzliche Felder. -
Mischen von automatischen und benutzerdefinierten Accessoren: C# 14 erlaubt, dass ein Accessor automatisch implementiert ist und der andere einen Body verwendet, der
fieldbenutzt. Beispielsweise können Sie einen benutzerdefiniertensetbereitstellen undgetautomatisch lassen, oder umgekehrt. Der Compiler erzeugt, was für den nicht geschriebenen Accessor nötig ist. Das war früher nicht möglich; vorher musste man, sobald man einem Accessor einen Body gab, für beide eine explizite Implementierung liefern.
Insgesamt verbessert field die Lesbarkeit und Wartbarkeit, indem redundanter Code wegfällt und der Fokus nur auf dem benötigten benutzerdefinierten Verhalten liegt. Konzeptionell ist es vergleichbar mit der Funktionsweise des Schlüsselworts value in einem Setter (das den zugewiesenen Wert repräsentiert); hier steht field für die zugrunde liegende Speicherung der Eigenschaft.
Vorher vs. nachher: manuelles Hintergrundfeld vs. field-Schlüsselwort
Um den Unterschied zu sehen, vergleichen wir, wie Sie eine Eigenschaft, die eine Regel erzwingt, vor C# 14 deklariert hätten und nach der Einführung des neuen field-Schlüsselworts.
Szenario: Angenommen, wir wollen eine Eigenschaft Hours, die niemals auf eine negative Zahl gesetzt werden darf. In älteren C#-Versionen hätten wir Folgendes geschrieben:
Vor C# 14, mit manuellem Hintergrundfeld:
public class TimePeriodBefore
{
private double _hours; // backing field
public double Hours
{
get { return _hours; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException(nameof(value), "Value must not be negative");
_hours = value;
}
}
}
In diesem Code vor C# 14 mussten wir ein privates Feld _hours einführen, um den Wert zu speichern. Der Getter der Eigenschaft gibt dieses Feld zurück, und der Setter führt eine Prüfung durch, bevor er _hours zuweist. Das funktioniert, ist aber wortreich: Wir haben zusätzlichen Code, um _hours zu deklarieren und zu verwalten, und _hours ist überall in der Klasse zugänglich (das heißt, andere Methoden könnten in _hours schreiben und die Validierungslogik umgehen, wenn man nicht aufpasst).
Ab C# 14, mit dem field-Schlüsselwort:
public class TimePeriod
{
public double Hours
{
get; // auto-implemented getter (compiler provides it)
set => field = (value >= 0)
? value
: throw new ArgumentOutOfRangeException(nameof(value), "Value must not be negative");
}
}
Hier ist die Eigenschaft Hours ohne explizites Hintergrundfeld deklariert. Wir verwenden get; ohne Body, was auf einen automatischen Getter hinweist, und stellen einen Body für set bereit, der field nutzt. Der Ausdruck field = ... im Setter weist den Compiler an, dem Hintergrundfeld der Eigenschaft etwas zuzuweisen. Der Compiler erzeugt im Hintergrund automatisch ein privates Feld und implementiert den get-Accessor so, dass er dieses Feld zurückgibt. Im obigen Setter werfen wir bei negativem value eine Ausnahme; andernfalls weisen wir field zu (das den Wert speichert). Wir mussten _hours nicht selbst deklarieren, und der Body des Getters muss auch nicht geschrieben werden; das übernimmt der Compiler. Das Ergebnis ist eine prägnantere Eigenschaftsdefinition mit demselben Verhalten.
Beachten Sie, wie viel klarer die C#-14-Variante ist:
- wir haben das explizite Feld
_hoursentfernt; der Compiler kümmert sich darum. - der
get-Accessor bleibt ein einfacher, automatisch implementierter (get;), den der Compiler in “gib das Hintergrundfeld zurück” verwandelt. - der
set-Accessor enthält nur die Logik, die uns interessiert (die Prüfung auf nicht negativ); die eigentliche Zuweisung an die Speicherung erledigtfield = value.
Sie können field bei Bedarf auch in einem get-Accessor verwenden. Um beispielsweise eine Lazy-Initialisierung zu implementieren, könnten Sie etwa Folgendes schreiben:
public string Name
{
get => field ??= "Unknown";
set => field = value;
}
In diesem Fall weist der Getter beim ersten Zugriff auf Name, falls noch nicht gesetzt, dem Hintergrundfeld einen Standardwert "Unknown" zu und gibt ihn zurück. Folgende Lese- oder Schreibzugriffe nutzen dasselbe field. Ohne dieses Feature hätten Sie ein privates Feld und mehr Code im Getter gebraucht, um dasselbe Verhalten zu erreichen.
Wie behandelt der Compiler das field-Schlüsselwort?
Wenn Sie field in einem Eigenschafts-Accessor verwenden, erzeugt der Compiler im Hintergrund ein verborgenes Hintergrundfeld für diese Eigenschaft (sehr ähnlich wie bei einer automatisch implementierten Eigenschaft). Dieses Feld sehen Sie in Ihrem Quellcode nie, der Compiler vergibt aber einen internen Namen (z. B. so etwas wie <Hours>k__BackingField) und nutzt es, um den Wert der Eigenschaft zu speichern. Folgendes passiert unter der Haube:
- Erzeugung des Hintergrundfelds: Verwendet mindestens ein Accessor einer Eigenschaft
field(oder haben Sie eine automatisch implementierte Eigenschaft ohne Bodies), erzeugt der Compiler ein privates Feld zur Speicherung des Werts. Sie müssen dieses Feld nicht deklarieren. Im obigenTimePeriod.Hours-Beispiel würde der Compiler ein Feld zum Speichern des Stundenwerts erzeugen, und sowohl derget- als auch derset-Accessor arbeiten mit diesem Feld (entweder implizit oder über dasfield-Schlüsselwort). - Implementierung von Getter/Setter:
- Für einen automatisch implementierten Accessor (wie
get;oderset;ohne Body) erzeugt der Compiler automatisch die einfache Logik, um das Hintergrundfeld zurückzugeben oder zu setzen. - Für einen Accessor, in dem Sie einen Body mit
fieldangegeben haben, integriert der Compiler Ihre Logik und behandeltfieldim erzeugten Code als Verweis auf das Hintergrundfeld. Beispielsweise wirdset => field = value;im kompilierten Output zu etwas wieset { backingField = value; }, wobei jegliche zusätzliche Logik, die Sie geschrieben haben, drumherum erhalten bleibt. - Sie können automatische und benutzerdefinierte Accessoren mischen. Wenn Sie zum Beispiel einen Body für
setschreiben (mitfield) undgetalsget;lassen, erzeugt der Compiler dengetfür Sie. Umgekehrt könnten Sie einen benutzerdefiniertengetschreiben (z. B.get => ComputeSomething(field)) und einen automatisch implementiertenset;haben; in diesem Fall erzeugt der Compiler den Setter so, dass er einfach das Hintergrundfeld zuweist.
- Für einen automatisch implementierten Accessor (wie
- Verhalten ist äquivalent zu manuellen Feldern: Das Kompilat mit
fieldist im Wesentlichen dasselbe, als hätten Sie manuell ein privates Feld geschrieben und es in Ihrer Eigenschaft verwendet. Es gibt keine Performance-Strafe und keine Magie, abgesehen davon, dass Ihnen Boilerplate erspart bleibt. Es ist rein eine Annehmlichkeit zur Kompilierzeit. Beispielsweise kompilieren die beidenHours-Implementierungen oben (mit und ohnefield) zu sehr ähnlichem IL-Code; beide haben ein privates Feld für den Wert und Eigenschafts-Accessoren, die dieses Feld manipulieren. Der Unterschied: Den Code für eine der beiden hat der C#-14-Compiler für Sie geschrieben. - Eigenschaftsinitialisierer: Wenn Sie einen Initialisierer auf einer Eigenschaft verwenden, die
fieldnutzt (zum Beispielpublic int X { get; set => field = value; } = 42;), initialisiert der Initialisierer direkt das Hintergrundfeld bevor der Konstruktor läuft, genauso wie bei traditionellen Auto-Properties. Die Setter-Logik wird während der Objektkonstruktion nicht aufgerufen. (Das ist wichtig, wenn Ihr Setter Seiteneffekte hat; diese werden für den Initialwert via Initialisierer nicht ausgeführt. Falls Sie wollen, dass die Setter-Logik bei der Initialisierung läuft, weisen Sie die Eigenschaft im Konstruktor zu, anstatt einen Initialisierer zu verwenden.) - Attribute am Hintergrundfeld: Möchten Sie Attribute auf das erzeugte Hintergrundfeld anwenden, erlaubt C# feldgerichtete Attribute mit der Syntax
[field: ...]. Das war bereits bei Auto-Properties möglich und funktioniert auch hier. Beispielsweise können Sie[field: NonSerialized] public int Id { get; set => field = value; }schreiben, um das automatisch erzeugte Feld als nicht serialisierbar zu markieren. (Das funktioniert nur, wenn tatsächlich ein Hintergrundfeld für die Eigenschaft existiert, also mindestens ein Accessorfieldverwendet oder es eine Auto-Property ist.)
TLDR: Der Compiler erzeugt ein privates Hintergrundfeld und verdrahtet Ihre Eigenschafts-Accessoren so, dass sie es verwenden. Sie erhalten die Funktionalität einer vollständigen Eigenschaft mit nur einem Bruchteil des Codes. Implementierungstechnisch bleibt die Eigenschaft eine echte automatische Eigenschaft; Sie haben nur einen Hook erhalten, um Logik einzuhängen.
Syntax- und Verwendungsregeln für field
Beim Einsatz des field-Schlüsselworts beachten Sie folgende Regeln und Einschränkungen:
- Nur in Eigenschafts-/Indexer-Accessoren:
fieldkann nur im Body eines Eigenschafts- oder Indexer-Accessors verwendet werden (im Codeblock oder Ausdruck fürget,setoderinit). Es ist ein kontextuelles Schlüsselwort, d. h. außerhalb eines Eigenschafts-Accessors hatfieldkeine besondere Bedeutung (es würde einfach als Bezeichner gewertet). Wenn Sie versuchen,fieldin einer normalen Methode oder außerhalb einer Eigenschaft zu verwenden, erhalten Sie einen Kompilierfehler; der Compiler weiß dann nicht, welches Hintergrundfeld gemeint ist. - Kontextuelles Schlüsselwort (nicht vollständig reserviert): Da
fieldkein global reserviertes Schlüsselwort ist, könnten Sie technisch gesehen Variablen oder Mitglieder namensfieldan anderer Stelle haben. Innerhalb eines Eigenschafts-Accessors wirdfieldjedoch als Schlüsselwort behandelt und verweist auf das Hintergrundfeld, nicht auf eine Variable namensfield. Siehe “Namenskonflikte” weiter unten zum Umgang mit diesem Szenario. - Verwendung in get/set/init-Accessoren: Sie können
fieldin einemget-,set- oderinit-Accessor verwenden. In einem Setter oder Init-Accessor wirdfieldtypischerweise zugewiesen (z. B.field = value;). In einem Getter könnten Siefieldzurückgeben oder verändern (z. B.return field;oderfield ??= defaultValue;). Sie könnenfieldin nur einem Accessor oder in beiden verwenden, je nach Bedarf:- Verwenden Sie
fieldnur in einem Accessor, können Sie den anderen als automatisch implementiert (get;oderset;ohne Body) belassen, und der Compiler erzeugt trotzdem das Hintergrundfeld und verdrahtet alles. - Verwenden Sie
fieldin beiden Accessoren, ist das ebenfalls in Ordnung; Sie schreiben dann effektiv die Logik für Get und Set aus (aber weiterhin ohne das Feld manuell zu deklarieren). Das kann sinnvoll sein, wenn sowohl Lesen als auch Schreiben Sonderbehandlung brauchen. Beispielsweise könnte ein Setter eine Bedingung erzwingen und ein Getter bei einem ersten Zugriff eine Transformation oder Lazy Loading durchführen, beide unter Nutzung desselbenfield.
- Verwenden Sie
fieldist außerhalb des Accessors nicht referenzierbar: Sie können denfield-Verweis nicht speichern und woanders verwenden, und Sie können auch nicht direkt außerhalb der Eigenschaft auf das vom Compiler erzeugte Hintergrundfeld zugreifen. Dieses Hintergrundfeld ist in Ihrem Quellcode quasi anonym (auch wenn der Compiler ihm einen internen Namen gibt). Wenn Sie mit dem Wert interagieren müssen, tun Sie das über die Eigenschaft oder innerhalb ihrer Accessoren mitfield.- Nicht für Events: Das
field-Schlüsselwort ist für Eigenschaften (und Indexer) gedacht. Für die add/remove-Accessoren von Events ist es nicht verfügbar. (Auch Events in C# können Hintergrundfelder für den Delegate haben, aber das Sprachteam hat sich gegen eine Erweiterung vonfieldauf Event-Accessoren entschieden.) - Nicht mit expliziten Felddeklarationen mischen: Wenn Sie sich entscheiden, ein eigenes Hintergrundfeld für eine Eigenschaft zu deklarieren, sollten Sie
fieldnicht in den Accessoren dieser Eigenschaft verwenden. In dem Fall verweisen Sie einfach wie traditionell mit dem Namen auf Ihr explizites Feld. Dasfield-Schlüsselwort soll in solchen Szenarien gerade die Notwendigkeit eines expliziten Felds ersetzen. Anders gesagt: Eine Eigenschaft hat entweder ein implizites, vom Compiler verwaltetes Feld (wenn Siefieldoder Auto-Accessoren verwenden) oder Sie verwalten es selbst, aber nicht beides.
Kurz gesagt: Verwenden Sie field innerhalb Ihrer Eigenschafts-Accessoren, um auf den verborgenen Speicher dieser Eigenschaft zu verweisen, und sonst nirgends. Für alles außerhalb von Eigenschaften gelten die normalen C#-Sichtbarkeitsregeln.
Umgang mit Namenskonflikten (wenn Sie eine eigene field-Variable haben)
Da field in älteren C#-Versionen kein reserviertes Wort war, ist es möglich (wenn auch ungewöhnlich), dass etwas Code “field” als Variablen- oder Feldnamen verwendet hat. Mit der Einführung des kontextuellen Schlüsselworts field in Accessoren könnte solcher Code mehrdeutig werden oder brechen. Das Sprachdesign berücksichtigt das:
fieldin einem Accessor verdeckt Bezeichner: Innerhalb der Eigenschafts-Accessoren verdeckt das neue Schlüsselwortfieldjeden Bezeichner namensfield, den Sie in diesem Scope haben könnten. Wenn Sie etwa eine lokale Variable oder einen Parameter namensfieldin einem Setter haben (vielleicht aus altem Code), interpretiert der Compilerfieldjetzt als Schlüsselwort für das Hintergrundfeld, nicht als Ihre Variable. In C# 14 führt das zu einem Kompilierfehler, wenn Sie versuchen, eine Variable namensfieldin einem Accessor zu deklarieren oder zu verwenden, weilfieldjetzt als Schlüsselwort erwartet wird.- Mit
@fieldoderthis.fieldauf das tatsächliche Feld verweisen: Wenn Sie wirklich ein Mitgliedsfeld in Ihrer Klasse haben, das wörtlich “field” heißt (nicht empfohlen, aber möglich), oder eine Variable namens “field” im Scope, können Sie sie weiterhin referenzieren, indem Sie den Namen escapen. C# erlaubt, einem Bezeichner ein@voranzustellen, um ihn auch dann zu verwenden, wenn er ein Schlüsselwort ist. Hat Ihre Klasse z. B.private int field;und Sie müssen es in einem Accessor referenzieren, schreiben Sie@field, um es als Bezeichner anzusprechen. Ebenso können Siethis.fieldnutzen, um explizit auf das Mitgliedsfeld zu verweisen. Mit@oder einer Qualifizierung umgehen Sie die Interpretation als kontextuelles Schlüsselwort und greifen auf die tatsächliche Variable zu.
private int field = 10; // a field unfortunately named "field"
public int Example
{
get { return @field; } // use @field to return the actual field
set { @field = value; } // or this.field = value; either works
}
- Wenn Sie aber die Möglichkeit haben, ist es besser, das Mitglied umzubenennen, um Verwirrung zu vermeiden. In modernem C# sollte
fieldallein in einem Accessor dem Hintergrundfeld des Compilers vorbehalten bleiben. Wenn Sie eine ältere Codebasis auf C# 14 aktualisieren, warnt Sie der Compiler, wenn er Verwendungen vonfieldfindet, die zuvor auf etwas anderes verwiesen, und weist Sie darauf hin, sie zu disambiguieren. - Den Namen ganz vermeiden: Als generelle Best Practice sollten Sie
fieldnicht als Bezeichner in Ihrem Code verwenden. Da es jetzt (im Kontext) ein Schlüsselwort ist, würde die Behandlung als gewöhnlicher Name Leser verwirren und kann zu Fehlern führen. Wenn Sie bislangfieldals Variablennamen verwendet haben, denken Sie beim Wechsel zu C# 14 ans Umbenennen. Übliche Namenskonventionen (etwa private Felder mit_zu präfixieren) verhindern diesen Konflikt in den meisten Fällen ohnehin.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.