Roslyn, die .NET Compiler Platform von Microsoft, ist seit vielen Jahren ein zentraler Bestandteil der C#-Entwicklungslandschaft. Sie bietet nicht nur Compiler-Funktionalität, sondern auch APIs zur Codeanalyse und -generierung. Eine der neuesten und leistungsfähigsten Erweiterungen in Roslyn sind die Source Generators, die Entwicklern ermöglichen, zur Kompilierzeit zusätzlichen Code zu generieren.
Eine Weiterentwicklung der Source Generators stellen die Incremental Source Generators dar. Sie bieten eine noch effizientere und skalierbarere Art der Code-Generierung, indem sie nur das neu berechnen, was sich im Quellcode geändert hat. Dieser Artikel gibt einen Überblick darüber, wie Incremental Source Generators funktionieren, was ihre Vorteile sind und wie man sie in der Praxis einsetzt.
Was sind Roslyn Source Generators?
Source Generators ermöglichen es, zusätzlichen C#-Code während des Kompilierens zu erzeugen. Dies ist besonders nützlich, wenn sich bestimmte Muster im Code wiederholen oder komplexe API-Strukturen automatisiert erstellt werden müssen. Beispiele dafür sind:
- Automatische Generierung von Boilerplate-Code (z.B. für INotifyPropertyChanged).
- Erstellung von Serialisierungs- und Deserialisierungscode für bestimmte Klassen.
- Datenbankzugriffsschichten, die basierend auf Modellen generiert werden.
Die erste Version der Source Generators arbeitete auf der Grundlage des gesamten Syntaxbaums und verarbeitete jedes Mal den gesamten Code, was in größeren Projekten zu Performanceproblemen führen konnte.
Einführung von Incremental Source Generators
Incremental Source Generators verbessern dieses Konzept durch eine inkrementelle Arbeitsweise. Anstatt den gesamten Code bei jeder Kompilierung neu zu verarbeiten, berechnen sie nur die Änderungen, die seit der letzten Kompilation vorgenommen wurden. Dies führt zu erheblichen Leistungssteigerungen, besonders bei großen Codebasen.
Die Grundidee ist, dass die verschiedenen Schritte der Code-Generierung in unabhängige, deterministische Schritte aufgeteilt werden, die nur dann erneut ausgeführt werden, wenn sich die Daten, auf denen sie basieren, ändern.
Wie funktionieren Incremental Source Generators?
Im Gegensatz zu klassischen Source Generators verwenden Incremental Source Generators das Konzept der Pipeline-Verarbeitung. Die Pipeline ist eine Abfolge von Transformationsschritten, die auf die Eingabe-Daten (z.B. den Syntaxbaum) angewendet werden. Jeder Schritt kann den Output des vorhergehenden Schritts nutzen und nur dann erneut ausgeführt werden, wenn sich die Eingabe für diesen Schritt geändert hat.
Pipeline-Prinzip
Eine Incremental Pipeline besteht aus den folgenden Schritten:
- Input sammeln: Zuerst werden Daten aus der Compilation oder dem Syntaxbaum gesammelt, wie z.B. Attribute oder bestimmte Klassen.
- Transformation: Diese Daten werden dann in einer Reihe von Schritten transformiert, z.B. um Codefragmente zu erzeugen oder weitere Informationen zu sammeln.
- Code-Generierung: Am Ende der Pipeline wird der generierte Code ausgegeben. Jeder dieser Schritte ist inkrementell, d.h. er wird nur ausgeführt, wenn sich die Eingabedaten geändert haben.
Beispiel einer Incremental Source Generator Pipeline
Angenommen, wir möchten für jede Klasse, die ein bestimmtes Attribut (z.B. [GenerateToString]) verwendet, eine ToString-Methode generieren.
Sammeln der Klassen mit dem Attribut Im ersten Schritt der Pipeline werden alle Klassen gesammelt, die mit dem Attribut [GenerateToString] dekoriert sind.
Transformation der gesammelten Daten Im zweiten Schritt extrahieren wir die relevanten Informationen aus diesen Klassen (z.B. die Namen der Felder und Eigenschaften).
Generierung der ToString-Methoden Im letzten Schritt wird für jede dieser Klassen eine ToString-Methode generiert und dem Kompilat hinzugefügt.
Vorteile von Incremental Source Generators
Leistungsoptimierung Der Hauptvorteil ist die drastische Verbesserung der Performance. Da nur geänderte Daten verarbeitet werden, reduziert sich die Kompilierungszeit erheblich, insbesondere in großen Projekten.
Deterministische Code-Generierung Da die einzelnen Schritte klar voneinander getrennt sind, ist das Verhalten der Generatoren vorhersehbarer und leichter zu debuggen.
Bessere Skalierbarkeit Durch die inkrementelle Verarbeitung können Generatoren in Projekten mit Tausenden von Klassen und Attributen effizienter arbeiten.
Geringerer Speicherverbrauch Da nur geänderte Teile des Codes erneut analysiert und generiert werden, wird auch der Speicherverbrauch reduziert.
Implementierung eines Incremental Source Generators
Um einen Incremental Source Generator zu implementieren, muss die Schnittstelle IIncrementalGenerator verwendet werden. Diese stellt eine Methode Initialize bereit, in der die Pipeline konfiguriert wird.
Ein Beispielcode könnte folgendermaßen aussehen:
[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Sammle alle Klassen, die mit [GenerateToString] dekoriert sind
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (syntaxNode, _) => syntaxNode is ClassDeclarationSyntax classDecl &&
classDecl.AttributeLists.Any(),
transform: (context, _) => context.Node as ClassDeclarationSyntax)
.Where(classDecl => classDecl != null);
// Transformiere die gesammelten Daten
var toStringImplementations = classDeclarations
.Select((classDecl, _) => GenerateToStringMethod(classDecl));
// Füge den generierten Code zur Compilation hinzu
context.RegisterSourceOutput(toStringImplementations, (context, source) =>
{
context.AddSource($"{source.ClassName}_ToString.g.cs", source.GeneratedCode);
});
}
private (string ClassName, string GeneratedCode) GenerateToStringMethod(ClassDeclarationSyntax classDecl)
{
// Erzeuge eine einfache ToString-Methode basierend auf den Feldern und Eigenschaften
var className = classDecl.Identifier.Text;
var properties = classDecl.Members.OfType<PropertyDeclarationSyntax>().Select(prop => prop.Identifier.Text);
var fields = classDecl.Members.OfType<FieldDeclarationSyntax>().Select(field => field.Identifier.Text);
var body = string.Join(", ", properties.Concat(fields).Select(name => $"\"{name} = \" + this.{name}"));
var generatedCode = $@"
namespace {classDecl.SyntaxTree.GetRoot().Namespace()}
{{
public partial class {className}
{{
public override string ToString()
{{
return $""{className}: {body}"";
}}
}}
}}";
return (className, generatedCode);
}
}
Fazit
Incremental Roslyn Source Generators sind eine leistungsstarke Erweiterung des Roslyn-Ökosystems. Sie bieten eine effiziente und skalierbare Möglichkeit, zur Kompilierzeit Code zu generieren und sind besonders in großen Projekten von Vorteil. Durch die inkrementelle Verarbeitung und das Pipeline-Prinzip lassen sich Performanceprobleme vermeiden, die bei herkömmlichen Source Generators auftreten können. Mit der Einführung dieser Generatoren wird die Code-Generierung in C# sowohl einfacher als auch ressourcenschonender.