Partielle Konstruktoren und Ereignisse in C# 14
C# 14 ermöglicht, Instanzkonstruktoren und Ereignisse als partielle Member zu deklarieren und Definitionen über mehrere Dateien aufzuteilen, für sauberere Codegenerierung und bessere Trennung der Belange.
C# 14 führt eine neue Möglichkeit ein, Instanzkonstruktoren und Ereignisse als partial-Member zu deklarieren. Das heißt, Sie können die Definition eines Konstruktors oder Ereignisses auf zwei Teile einer partiellen Klasse aufteilen, ähnlich wie C# das schon lange für partielle Methoden und partielle Eigenschaften erlaubt. Ein Teil einer partiellen Klasse kann einen Konstruktor oder ein Ereignis deklarieren, ein anderer Teil kann es implementieren. Das ist besonders nützlich in Szenarien wie der Codegenerierung, in denen eine Datei automatisch generiert und eine andere manuell von Entwicklern gepflegt wird.
Partielle Konstruktoren in C# 14
Partielle Konstruktoren erlauben Ihnen, einen Klassenkonstruktor in zwei Deklarationen innerhalb einer partial-Klasse aufzuteilen. Eine Deklaration liefert nur die Signatur (eine definierende Deklaration), eine andere den eigentlichen Rumpf (eine implementierende Deklaration). Der Compiler führt sie zusammen, sodass sie sich zur Laufzeit wie ein einziger Konstruktor verhalten.
Wichtige Regeln für partielle Konstruktoren:
- Genau eine definierende und eine implementierende Deklaration: Sie benötigen einen Teil der partiellen Klasse, der den Konstruktor deklariert (mit Semikolon und ohne Rumpf), und einen anderen Teil, der die Implementierung liefert (mit dem Codeblock des Konstruktors). Die Signaturen (Parametertypen, Namen usw.) müssen zwischen beiden exakt übereinstimmen. Fehlt der definierende oder der implementierende Teil (oder gibt es Duplikate), wird der Code nicht kompiliert.
- Nur in partiellen Klassen: Beide Deklarationen müssen sich in derselben Klasse befinden, die selbst als
partialmarkiert sein muss. In einer nicht-partiellen Klasse können Sie keinen partiellen Konstruktor haben. - Konstruktor-Initialisierer nur in der Implementierung: Wenn der Konstruktor einen anderen Konstruktor oder einen Konstruktor der Basisklasse über
: this(...)oder: base(...)aufrufen muss, darf dieser Initialisierer nur in der implementierenden Deklaration stehen. Der definierende (signaturbezogene) Teil darf keinerlei Konstruktor-Initialisierer enthalten. In der Praxis schreiben Sie jedenthis(...)- oderbase(...)-Aufruf in den Header des implementierenden Konstruktors. - Primary-Constructor-Syntax nur in einer Datei: C# 14 erlaubt Primary Constructors (Konstruktorparameter in der Klassendeklaration) in partiellen Klassen, jedoch darf nur eine partielle Datei die Parameterliste enthalten. Wenn Sie also die kompakte Primary-Constructor-Form in einer partiellen Klasse verwenden, muss sie in einer einzigen partiellen Klassendeklaration stehen; andere partielle Deklarationen derselben Klasse dürfen keine eigene Parameterliste wiederholen oder deklarieren.
Hier ein Beispiel für einen partiellen Konstruktor in Aktion. Stellen Sie sich eine Klasse vor, die auf zwei Dateien aufgeteilt ist: Ein automatisch generierter Teil deklariert einen Konstruktor, ein anderer Teil implementiert ihn.
// File: Car.AutoGenerated.cs (auto-generated partial class)
public partial class Car
{
// Defining partial constructor (signature only, no body)
public partial Car(string model);
}
// File: Car.cs (developer-maintained partial class)
public partial class Car
{
// Implementing partial constructor (actual body)
public partial Car(string model)
{
// (Optional) Can call another constructor or base constructor here
// : base() or : this(...) would be placed after the parameter list if needed.
if (string.IsNullOrEmpty(model))
throw new ArgumentException("Model must be provided", nameof(model));
Console.WriteLine($"Car model set to {model}");
}
}
In diesem Beispiel ist der Konstruktor der Klasse Car aufgeteilt: Der erste Teil deklariert, dass Car(string model) existiert, der zweite Teil liefert die Implementierung, die das Argument prüft und eine Nachricht ausgibt. Die definierende Deklaration darf keinen Code enthalten — sie ist nur die Signatur, die mit ; endet. Die implementierende Deklaration enthält den Rumpf und könnte zusätzlich einen Aufruf der Basisklasse enthalten (wenn Car von einer anderen Klasse erbte) oder einen anderen Konstruktor über this() aufrufen. Zur Kompilierzeit stellt der Compiler sicher, dass es genau einen Teil von jeder Sorte gibt, und behandelt den Konstruktor als einen einzigen einheitlichen Konstruktor.
Partielle Ereignisse in C# 14
Partielle Ereignisse erlauben eine ähnliche Aufteilung der Definition für Ereignisse in einer partiellen Klasse. Ein Teil der Klasse definiert das Ereignis (ohne add/remove-Logik, wie eine normale automatische Ereignisdeklaration), und ein anderer Teil liefert die add/remove-Accessoren (den Code, der ausgeführt wird, wenn Abonnenten Event-Handler anhängen oder entfernen).
Wichtige Regeln für partielle Ereignisse:
- Genau eine definierende und eine implementierende Deklaration: Wie bei Konstruktoren muss ein als
partialmarkiertes Ereignis in der partiellen Klasse genau zweimal deklariert werden: eine Definition ohne Rumpf und eine Implementierung mit den Accessor-Rümpfen. Beide müssen denselben Ereignistyp und denselben Namen haben und in derselben partiellen Klasse erscheinen. Zusätzliche partielle Ereignisteile über diese beiden hinaus sind nicht erlaubt. - Die definierende Deklaration ist feldartig: Der definierende Teil eines partiellen Ereignisses wird wie eine normale feldgestützte Ereignisdeklaration geschrieben, zum Beispiel:
public partial event EventHandler SomethingHappened;(mit Semikolon und ohne Accessor-Block). Da es jedoch partiell ist, generiert der Compiler weder ein automatisches Hintergrundfeld noch eine Standard-add/remove-Logik für dieses Ereignis. Es ist im Wesentlichen ein Platzhalter, der angibt: “Es wird ein Ereignis dieses Namens und Typs geben.” - Die implementierende Deklaration muss Accessoren haben: Der implementierende Teil des partiellen Ereignisses muss sowohl einen
add- als auch einenremove-Accessor in einem Ereignisblock liefern, wie bei einem manuell implementierten Ereignis. Hier schreiben Sie, was geschieht, wenn Hörer abonnieren oder ihr Abonnement beenden. Da der definierende Teil kein implizites Hintergrundfeld besitzt, muss Ihre Implementierung typischerweise die Speicherung der Abonnenten verwalten (zum Beispiel über ein privates Feld, eine Liste oder einen anderen Mechanismus) oder an eine andere Ereignisquelle weiterleiten. - Nur in partiellen Klassen (und nicht als abstract oder Interface-Implementierung): Wie bei partiellen Konstruktoren können partielle Ereignisse nur innerhalb einer als
partialmarkierten Klasse verwendet werden. Sie dürfen ein partielles Ereignis auch nicht alsabstractmarkieren und könnenpartialnicht zur Implementierung eines Ereignisses einer Schnittstelle verwenden — das Feature ist für die Aufteilung innerhalb einer Klasse gedacht, nicht über Schnittstellengrenzen hinweg.
Hier ein Beispiel, das ein partielles Ereignis veranschaulicht. Eine Datei deklariert das Ereignis, eine andere implementiert die individuelle add/remove-Logik:
// File: Sensor.AutoGenerated.cs (auto-generated partial class)
public partial class Sensor
{
// Defining partial event (no body, just declaration)
public partial event EventHandler DataReceived;
}
// File: Sensor.cs (developer-maintained partial class)
public partial class Sensor
{
// Implementing partial event (with custom add/remove logic)
private EventHandler _dataReceivedHandlers; // backing storage for event handlers
public partial event EventHandler DataReceived
{
add
{
Console.WriteLine("Subscriber added to DataReceived");
_dataReceivedHandlers += value;
}
remove
{
Console.WriteLine("Subscriber removed from DataReceived");
_dataReceivedHandlers -= value;
}
}
// Optional: a method to raise the event safely from this class
protected void OnDataReceived(EventArgs e)
{
_dataReceivedHandlers?.Invoke(this, e);
}
}
In diesem Beispiel besitzt die Klasse Sensor ein DataReceived-Ereignis, das auf zwei Dateien aufgeteilt ist. Der automatisch generierte Teil der Klasse deklariert, dass es ein DataReceived-Ereignis vom Typ EventHandler gibt. Der vom Entwickler verwaltete Teil liefert die eigentliche Implementierung: Er definiert ein privates Feld zur Speicherung der Abonnenten und implementiert die add- und remove-Blöcke, um eine Nachricht zu protokollieren und das private Feld zu aktualisieren. Wenn anderer Code sensor.DataReceived += Handler; ausführt, läuft die individuelle add-Logik. Genauso löst das Aufheben des Abonnements die individuelle remove-Logik aus. Die Klasse kann _dataReceivedHandlers verwenden, um das Ereignis auszulösen (zum Beispiel in der Methode OnDataReceived). Ohne den implementierenden Teil ließe sich die definierende Deklaration allein nicht kompilieren, da das Ereignis für sich genommen keine Stütze hat — C# verlangt, dass der implementierende Teil die Funktionalität des Ereignisses vervollständigt.
Vergleich mit früheren C#-Versionen
Vor C# 14 konnten Sie Konstruktoren oder Ereignisse nicht als partial deklarieren. In C# 13 und früher war das Schlüsselwort partial nur für Klassen, Methoden und (ab C# 13) Eigenschaften/Indexer gültig. Versuchten Sie, einen Konstruktor oder ein Ereignis als partial zu markieren, gab der Compiler einen Fehler aus. Diese Einschränkung bedeutete, dass es keine Möglichkeit gab, die Implementierung eines Konstruktors oder Ereignisses auf verschiedene Dateien aufzuteilen.
Entwickler griffen oft zu Workarounds, um eine ähnliche Flexibilität zu erreichen. Stellen Sie sich beispielsweise ein Szenario vor, in dem automatisch generierter Code beim Aufbau eines Objekts benutzerdefinierte Logik ausführen soll. Ohne partielle Konstruktoren bestand ein gängiges Muster darin, eine partielle Methode im Konstruktor zu verwenden. Der automatisch generierte Teil einer Klasse konnte diese Methode aufrufen, während die Methodenimplementierung in einer anderen Datei lag. In älterem C#-Code könnten Sie zum Beispiel Folgendes sehen:
// Before C# 14: using a partial method to extend constructor logic
public partial class Car
{
public Car()
{
InitializeComponents(); // auto-generated initialization
OnConstructed(); // call a partial method hook
}
// This partial method can be implemented in another file to add custom behavior
partial void OnConstructed();
}
In der zweiten Datei konnte der Entwickler die partielle Methode OnConstructed implementieren, um nach InitializeComponents() zusätzlichen Code auszuführen. Dieses Muster erlaubte das Einschleusen benutzerdefinierten Codes zur Konstruktionszeit, war aber indirekt und etwas umständlich. Für Ereignisse gab es ebenfalls keinen einfachen Workaround — man musste entweder erben und die Funktionalität überschreiben oder den generierten Code verändern, da man die add/remove-Logik eines Ereignisses nicht über mehrere Dateien aufteilen konnte.
Mit den partiellen Konstruktoren und Ereignissen in C# 14 sind diese Workarounds nicht mehr notwendig. Sie können einen Konstruktor oder ein Ereignis direkt als partial deklarieren und die Implementierung separat liefern, was den Code geradliniger macht. Der neue Ansatz ist klarer und macht Dummy-Partialmethoden oder andere Tricks überflüssig.
Anwendungsfälle und Vorteile
Die Hauptnutznießer von partiellen Konstruktoren und Ereignissen sind Szenarien rund um Codegenerierung und Tooling. Viele Frameworks und Werkzeuge generieren C#-Code (zum Beispiel UI-Designer, ORMs oder Werkzeuge zum Scaffolding von Schnittstellen) und markieren Klassen oft als partial, damit Entwickler die generierten Klassen erweitern können, ohne den automatisch generierten Code zu bearbeiten. Mit C# 14:
- Codegeneratoren können Konstruktoren für Sie definieren: Beispielsweise könnte ein Source Generator eine Signatur eines partiellen Konstruktors erstellen, die der Entwickler dann mit benutzerdefinierter Initialisierungslogik implementiert. Umgekehrt könnte ein Generator einen vom Entwickler deklarierten partiellen Konstruktor implementieren, etwa um framework-spezifischen Setup-Code im Hintergrund einzubringen. So lassen sich generierte und vom Nutzer geschriebene Initialisierungsschritte sicher kombinieren.
- Komplexe Konstruktionslogik aufteilen: In großen Projekten möchten Sie Konstruktorlogik vielleicht über mehrere Dateien organisieren (zum Beispiel Trennung zwischen Dependency-Verdrahtung und Geschäftslogik). Partielle Konstruktoren bieten dafür einen strukturierten Weg, falls erforderlich.
- Benutzerdefinierte Ereignismuster: Partielle Ereignisse ermöglichen fortgeschrittene Szenarien der Ereignisbehandlung. Ein Paradebeispiel ist das Weak-Event-Muster (zur Vermeidung von Speicherlecks bei Ereignissen). Ein Entwickler oder Source Generator kann ein partielles Ereignis in einer Datei deklarieren (vielleicht mit einer Annotation wie
[WeakEvent]) und es in einer anderen Datei so implementieren, dassadd/removeschwache Referenzen auf Handler verwenden. Dadurch verhindern abonnierende Objekte die Garbage Collection nicht, falls sie das Abonnement vergessen zu beenden. Vor partiellen Ereignissen erforderte die Umsetzung eines Weak-Event-Musters viel Boilerplate oder externe Bibliotheken. Jetzt könnte ein Bibliotheksautor einen Generator bereitstellen, der die komplexe add/remove-Logik automatisch in einer separaten partiellen Implementierung liefert, während der Code des Anwenders sauber bleibt. Genauso können partielle Ereignisse Ereignisabonnements an darunterliegende Systeme weiterleiten (zum Beispiel als Wrapper für ein Ereignis aus einer Low-Level-API, indem add/remove behandelt und mit der eigentlichen Quelle verbunden werden). - Plattform- und Interop-Code: In Interop-Szenarien (zum Beispiel Xamarin oder .NET-Interop mit nativen Bibliotheken) müssen generierte Klassen oft im Konstruktor in nativen Code aufrufen oder native Ereignisanbindungen verwalten. Partielle Konstruktoren erlauben, dass das Setup für den nativen Aufruf im generierten Code lebt, während der Rest des Konstruktors vom Anwender definiert wird (oder umgekehrt). Partielle Ereignisse können einer Klasse erlauben, ein .NET-Ereignis bereitzustellen, das tatsächlich von einem plattformspezifischen Ereignismechanismus in einer anderen partiellen Datei gestützt wird.
In all diesen Fällen erleichtern partielle Konstruktoren und Ereignisse die Wartung des Codes. Der automatisch generierte Code kann erklären, was existieren muss, und der handgeschriebene Code kann auf das beschränkt sein, was der Entwickler tatsächlich implementieren möchte. Außerdem entstehen durch partielle Member keine Laufzeitkosten — die Aufteilung existiert nur zur Kompilierzeit. Das resultierende Programm verhält sich, als hätten Sie den vollständigen Konstruktor oder das vollständige Ereignis an einer einzigen Stelle geschrieben.
Indem Instanzkonstruktoren und Ereignisse partial sein dürfen, rundet C# 14 das Feature-Set partieller Klassen so ab, dass nahezu alle Member-Typen abgedeckt sind. Diese Erweiterung verbessert die Fähigkeit der Sprache, eine saubere Trennung zwischen automatisch generiertem und benutzerdefiniertem Code zu unterstützen. Entwickler erhalten Flexibilität, um Klassenverhalten ohne Tricks zu organisieren und zu erweitern, und Source Generators bekommen ein mächtiges neues Werkzeug, um Funktionalität modular einzubringen. Ob Sie an großen Frameworks arbeiten oder Code einfach aus Gründen der Übersichtlichkeit aufteilen — partielle Konstruktoren und Ereignisse bieten in C# 14 einen ausdrucksstärkeren und bequemeren Ansatz.
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.