Verwenden von Visual Studio und Source Generator zum automatischen Generieren von Code
Betriebsumgebung
- Visuelles Studio
-
- Visual Studio 2022
- .NETTO
-
- .NET 8.0
Voraussetzungen
- Visuelles Studio
-
- Es funktioniert sogar mit einer etwas älteren Version
- .NETTO
-
- Es funktioniert sogar mit einer etwas älteren Version
Zuerst
Es gibt verschiedene Techniken zum automatischen Generieren von Code mit eigenen Definitionen, aber in diesem Artikel zeige ich Ihnen, wie Sie einen Source Generator verwenden. Einer der größten Vorteile eines Source-Generators besteht darin, dass er die Struktur des Quellcodes des aktuellen Projekts analysiert und darauf basierend neuen Code generiert. Wenn Sie beispielsweise eine neue Klasse erstellen, können Sie festlegen, dass der Code automatisch hinzugefügt wird, um der Klasse zu entsprechen. Sie können programmieren, welche Art von Code Sie generieren möchten, sodass Sie jede beliebige Form der automatischen Codegenerierung erstellen können.
Der Code wird im Wesentlichen automatisch im Hintergrund generiert und hinter den Kulissen in das Projekt integriert. Da er nicht sichtbar als Datei ausgegeben wird, wird er nicht verwendet, um den automatisch generierten Code für allgemeine Zwecke wiederzuverwenden (obwohl er vorerst durch Kopieren entfernt werden kann). Da der Code jedoch automatisch entsprechend der Struktur des Projekts generiert wird, reduziert er die Kosten für manuelle Eingaben und reduziert entsprechend Codeschreibfehler, was ein großer Vorteil ist.
In diesem Artikel werde ich erklären, wie Sie überprüfen können, ob der Code automatisch generiert wird, sodass ich nicht so weit gehen werde, den Code tatsächlich gründlich zu analysieren und eine erweiterte Ausgabe durchzuführen. Bitte schauen Sie selbst als Bewerbung nach.
Einrichtung
Installieren Sie zunächst Visual Studio. Eine kurze Erklärung ist in den folgenden Tipps zusammengefasst.
Grundsätzlich können Sie es für jedes Projekt verwenden, sodass es keine Rolle spielt, welche Workload Sie einrichten. Diesmal jedoch als "Einzelkomponente", ". NET Compiler Platform SDK. Dies ist nützlich für das Debuggen während der Entwicklung des Quellgenerators. Wenn Sie Visual Studio bereits installiert haben, können Sie es über das Visual Studio-Menü unter Extras > Abrufen von Tools und Features hinzufügen.
Erstellen und Vorbereiten eines Source-Generator-Projekts
Source Generator wird in einem Projekt erstellt, das vom Hauptanwendungsprojekt getrennt ist. Es spielt keine Rolle, ob Sie sie zuerst erstellen oder später weitere erstellen. In diesem Fall erstelle ich es aus dem Source Generator-Projekt.
Wählen Sie auf dem Bildschirm Neues Projekt erstellen die Option Klassenbibliothek aus.
Der Projektname kann beliebig sein, aber im Moment CodeGenerator
belassen wir es bei .
Für Klassenbibliotheken unterstützt Source Generator derzeit . NET Standard 2.0.
Nachdem Sie Ihr Projekt erstellt haben, rufen Sie das Paket mit Microsoft.CodeAnalysis.CSharp
NuGet ab.
Das Verhalten kann je nach Version unterschiedlich sein, aber es macht keinen Sinn, die alte Version weiter zu verwenden, daher werde ich die neueste Version verwenden.
Öffnen Sie dann die Projektdatei als Code.
Wenn Sie es öffnen, sehen Sie Folgendes.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
</ItemGroup>
</Project>
Fügen Sie es wie folgt hinzu: Fühlen Sie sich frei, alles hinzuzufügen, was Sie brauchen.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<!-- Roslyn によるコード解析を行う -->
<IsRoslynComponent>true</IsRoslynComponent>
<!-- 入れないと警告が表示されるので入れておく -->
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<!-- 最新の C# の記述を使いたいので入れておく -->
<LangVersion>Latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
</ItemGroup>
</Project>
Schreiben Sie als Nächstes den Code für die Klasse, die den Code automatisch generiert.
Zunächst einmal werden wir nur ein Framework erstellen, also bitte den bestehenden Code von Anfang Class1.cs
an neu schreiben oder einen neuen Code hinzufügen.
Der Code sollte wie folgt aussehen: Der Klassenname kann beliebig sein, aber es ist besser, einen Namen zu haben, der zeigt, welche Art von Code automatisch generiert wird.
SampleGenerator
Belassen Sie es vorerst als .
Initialize
In dieser Methode schreiben Sie den Codeanalyse- und Codegenerierungsprozess.
using Microsoft.CodeAnalysis;
namespace CodeGenerator
{
[Generator(LanguageNames.CSharp)]
public partial class SampleGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
}
}
}
Erstellen Sie als Nächstes ein Projekt für die Anwendung, für die Sie automatisch Code generieren möchten. Es kann alles sein, aber hier werden wir es einfach als Konsolenanwendung verwenden. Der Typ und die Version des Frameworks des Projekts, für das der Code automatisch generiert wird, entsprechen der allgemeinen Nummer.
Fügen Sie einen Verweis auf das Quellgeneratorprojekt aus dem anwendungsseitigen Projekt hinzu.
Einige Einstellungen können nicht in den Eigenschaften festgelegt werden, daher öffnen Sie die Projektdatei im Code.
Ich denke, es sieht so aus:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CodeGenerator\CodeGenerator.csproj" />
</ItemGroup>
</Project>
Fügen Sie die Einstellungen für das referenzierte Projekt wie folgt hinzu: Stellen Sie sicher, dass Sie keinen Fehler in der Syntax des XML-Codes machen.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CodeGenerator\CodeGenerator.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>False</ReferenceOutputAssembly>
</ProjectReference>
</ItemGroup>
</Project>
Öffnen Sie als Nächstes die Projekteigenschaften auf der "Code-Generator-Seite".
Klicken Sie auf den Link, um die Benutzeroberfläche für die Debugstarteigenschaften zu öffnen.
Löschen Sie das ursprüngliche Profil, da Sie es nicht verwenden möchten.
Fügen Sie ein neues Profil hinzu.
Wählen Sie Roslyn-Komponente aus.
Wenn Sie die Einstellungen bisher vorgenommen haben, sollten Sie das Anwendungsprojekt auswählen können, also wählen Sie es aus.
Dies ist das Ende der Vorbereitung.
Überprüfen Sie, ob Sie debuggen können
Öffnen Sie den Quellcodegeneratorcode, und Initialize
platzieren Sie einen Haltepunkt am Ende der Methode.
Lassen Sie uns den Quellgenerator debuggen.
Wenn der Prozess am Haltepunkt angehalten wird, können Sie bestätigen, dass Sie normal debuggen. Dies sollte die Entwicklung Ihres Quellgenerators einigermaßen einfach machen.
Lassen Sie uns vorerst einen festen Code ausgeben
Versuchen wir zunächst, einen festen Code einfach auszugeben. Es ist einfach, weil Sie den Code nicht einmal analysieren müssen. Auch wenn es sich um einen festen Code handelt, wird er als String behandelt, sodass es möglich ist, die Produktion von Code in fester Form durch das Erstellen eines Programms zu erhöhen.
Wenn Sie einen festen Code ausgeben möchten, können Sie context.RegisterPostInitializationOutput
dies mit tun.
Im Folgenden finden Sie ein Beispiel für die Codeausgabe.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace CodeGenerator;
[Generator(LanguageNames.CSharp)]
public partial class SampleGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// コードを解析せずそのままコードを出力するならこれを使用します
context.RegisterPostInitializationOutput(static postInitializationContext =>
{
// コード生成処理が中断されることもあるので CancellationToken 処理は入れたほうがいいです
System.Threading.CancellationToken token = postInitializationContext.CancellationToken;
token.ThrowIfCancellationRequested();
// 出力するコードを作ります
var source = """
internal static class SampleClass
{
public static void Hello() => Console.WriteLine("Hello Source Generator!!");
}
""";
// コードファイルに出力します (実際に目に見えるどこかのフォルダに出力されるわけではありません)
postInitializationContext.AddSource("SampleGeneratedFile.cs", SourceText.From(source, Encoding.UTF8));
});
}
}
Der Inhalt des Codes ist wie in den Kommentaren geschrieben, daher werde ich die Details weglassen. Erstellen Sie es und stellen Sie sicher, dass keine Fehler vorhanden sind.
Wenn der Build abgeschlossen ist, können Sie "Analysetools" im Anwendungsprojekt erweitern, um den vom Codegenerator generierten Code anzuzeigen.
Wenn es nicht angezeigt wird, starten Sie Visual Studio neu, und überprüfen Sie es. Es scheint, dass Visual Studio zu diesem Zeitpunkt möglicherweise nicht aktualisiert wird, es sei denn, Sie starten es neu. Die Intelligenz und die Syntaxhervorhebung, die beim Erstellen von Programmen verwendet werden, sind ähnlich. Da der Code selbst jedoch zum Zeitpunkt des Builds reflektiert wird, scheint das Programm auf der Anwendungsseite widergespiegelt zu werden.
Nachdem der Code generiert wurde, versuchen Sie, ihn in Ihrer Anwendung zu verwenden. Es sollte gut funktionieren.
Analysieren und Generieren von Code
Wenn Sie den Code einfach normal verarbeiten und ausgeben, unterscheidet er sich nicht wesentlich von anderen automatischen Codegenerierungen, und Sie können die Vorteile des Quellgenerators nicht nutzen. Lassen Sie uns nun den Code des Anwendungsprojekts analysieren und den Code entsprechend generieren.
Die Analyse des Codes ist jedoch ziemlich tief, sodass ich hier nicht alles erklären kann. Vorerst werde ich bis zu dem Punkt erklären, an dem Sie den Code analysieren und ausgeben können. Wenn Sie tiefer gehen möchten, recherchieren Sie bitte selbst.
Als Beispiel möchte ich vorerst automatisch den Code "Füge eine Methode zu Reset
allen erstellten Klassen hinzu und setze den Wert default
aller Eigenschaften auf zurück".
In den letzten Jahren scheint es, dass der Umgang mit unveränderlichen Instanzen bevorzugt wurde, aber in Spielprogrammen ist es nicht möglich, in jedem Frame eine neue Instanz zu erstellen, daher denke ich, dass es einen Nutzen für den Prozess des Zurücksetzens des Werts gibt.
default
Vielleicht möchten Sie etwas anderes einstellen, aber wenn Sie das tun, wird es ein langes erstes Beispiel, also wenden Sie es bitte selbst an.
Code-Generator-Seite
Erstellen Sie eine neue Klasse, um den zuvor erstellten Generator beizubehalten. Wenn sich der automatisch zu generierende Inhalt ändert, ist es besser, einen neuen in einer anderen Klasse zu erstellen.
Der Roslyn-Compiler kümmert sich um die Strukturierung des Codes, also werden wir hier die strukturierten Daten analysieren und den Code schreiben.
Zuerst werde ich den gesamten Code veröffentlichen. Ich habe versucht, so wenig Code wie möglich zu haben. Diesmal funktioniert es, aber es ist nur auf einer Ebene, die sich als Herr/Frau bewegt, so dass bei der tatsächlichen Entwicklung einige Teile fehlen werden. Bitte verbessern Sie dort nach Bedarf.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
using System.Text;
using System.Threading;
namespace CodeGenerator;
[Generator(LanguageNames.CSharp)]
public partial class Sample2Generator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 構文を解析し処理対象のノードをコード出力用に変換します
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: static (node, cancelToken) =>
{
// 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
cancelToken.ThrowIfCancellationRequested();
// クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
// また partial class であること
return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},
// 対象のノードをコード出力に必要な形に変換します
transform: static (contextSyntax, cancelToken) =>
{
// いつでもキャンセルできるようにしておく
cancelToken.ThrowIfCancellationRequested();
// 今回は ClassDeclarationSyntax 確定なのでそのままキャストします
ClassDeclarationSyntax classDecl = (ClassDeclarationSyntax)contextSyntax.Node;
// データと構文から宣言されたシンボルを取得します
// 今回利用場面がほとんどないですが取得しておくといろんな情報を取得できるので便利です
var symbol = Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(contextSyntax.SemanticModel, classDecl, cancelToken)!;
// 出力に必要な値を返します。大抵の型を渡すことができます
return (symbol, classDecl);
}
);
// 解析し変換した情報をもとにコードを出力します
context.RegisterSourceOutput(syntaxProvider, static (contextSource, input) =>
{
// いつでもキャンセルできるようにしておく
CancellationToken cancelToken = contextSource.CancellationToken;
cancelToken.ThrowIfCancellationRequested();
// 解析後に渡された値を受け取ります
var (symbol, classDecl) = input;
StringBuilder sbInitValues = new();
// プロパティを列挙して値を初期化するコードを生成
foreach (var syntax in classDecl.Members.OfType<PropertyDeclarationSyntax>())
{
sbInitValues.AppendLine($"{syntax.Identifier.ValueText} = default;");
}
// 出力するコードを作成
var source = @$"
{string.Join(" ", classDecl.Modifiers.Select(x => x.ToString()))} class {symbol.Name}
{{
public void Reset()
{{
{sbInitValues}
}}
}}";
// ファイル名はクラス名に合わせておきます。作成したコードを出力します
contextSource.AddSource($"{symbol.Name}.g.cs", source);
});
}
}
Ich habe in den Kommentaren darüber geschrieben, aber ich werde es an einigen Stellen erklären.
// 構文を解析し処理対象のノードをコード出力用に変換します
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: ...,
// 対象のノードをコード出力に必要な形に変換します
transform: ...
);
context.SyntaxProvider.CreateSyntaxProvider
um den Code im Anwendungsprojekt zu analysieren und so weit wie möglich zu strukturieren.
Alles ist in Knoten unterteilt, und wir bestimmen, welche davon wir predicate
verarbeiten möchten.
Konvertieren Sie bei Bedarf transform
das verarbeitete Objekt mit und übergeben Sie es an die Ausgabeseite.
predicate
In diesem Fall gehen wir wie folgt vor.
// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: static (node, cancelToken) =>
{
// 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
cancelToken.ThrowIfCancellationRequested();
// クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
// また partial class であること
return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},
Zunächst einmal kann der Prozess der automatischen Codegenerierung wie bei jedem Prozess immer vorzeitig abgebrochen werden.
Verwenden Sie also das Abbruchtoken zum ThrowIfCancellationRequested
Aufrufen, damit Sie jederzeit unterbrechen können.
predicate
Nachdem nun jeder Knoten aufgerufen ist, möchten wir bestimmen, welcher derjenige ist, den wir verarbeiten möchten.
Da es eine große Anzahl von ihnen gibt, ist es besser, sie hier einigermaßen einzugrenzen.
Da wir der Klasse dieses Mal eine Verarbeitung hinzufügen werden, bestimmen wir ClassDeclarationSyntax
, ob dies der Fall ist, und bestimmen, ob nur die Klasse verarbeitet wird.
partial class
Da der Code mit angehängt ist, wird er partial class
auch als Urteil gesetzt.
// 対象のノードをコード出力に必要な形に変換します
transform: static (contextSyntax, cancelToken) =>
{
// いつでもキャンセルできるようにしておく
cancelToken.ThrowIfCancellationRequested();
// 今回は ClassDeclarationSyntax 確定なのでそのままキャストします
ClassDeclarationSyntax classDecl = (ClassDeclarationSyntax)contextSyntax.Node;
// データと構文から宣言されたシンボルを取得します
// 今回利用場面がほとんどないですが取得しておくといろんな情報を取得できるので便利です
var symbol = Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(contextSyntax.SemanticModel, classDecl, cancelToken)!;
// 出力に必要な値を返します。大抵の型を渡すことができます
return (symbol, classDecl);
}
transform
ermöglicht es Ihnen, die Analyse in die erforderliche Form zu konvertieren und an die Codeausgabe zu übergeben.
Diesmal konvertieren wir nicht viel, sondern übergeben den Wert so an die Ausgabeseite, wie er ist return
.
Wir erhalten unterwegs "deklarierte Symbole", aberSemanticModel
wir können viele zusätzliche Informationen mit und Syntac
erhalten, also denke ich, dass wir sie bei Bedarf erhalten können.
return
Tupel werden erstellt und zurückgegeben, aber die Konfiguration der Daten, die an die Ausgabeseite übergeben werden sollen, kann beliebig sein.
Wenn Sie mehrere Daten übergeben möchten, können Sie ein Tupel wie dieses erstellen oder eine eigene Klasse definieren und übergeben.
// 解析し変換した情報をもとにコードを出力します
context.RegisterSourceOutput(syntaxProvider, static (contextSource, input) =>
{
// いつでもキャンセルできるようにしておく
CancellationToken cancelToken = contextSource.CancellationToken;
cancelToken.ThrowIfCancellationRequested();
// 解析後に渡された値を受け取ります
var (symbol, classDecl) = input;
StringBuilder sbInitValues = new();
// プロパティを列挙して値を初期化するコードを生成
foreach (var syntax in classDecl.Members.OfType<PropertyDeclarationSyntax>())
{
sbInitValues.AppendLine($"{syntax.Identifier.ValueText} = default;");
}
// 出力するコードを作成
var source = @$"
{string.Join(" ", classDecl.Modifiers.Select(x => x.ToString()))} class {symbol.Name}
{{
public void Reset()
{{
{sbInitValues}
}}
}}";
// ファイル名はクラス名に合わせておきます。作成したコードを出力します
contextSource.AddSource($"{symbol.Name}.g.cs", source);
});
context.RegisterSourceOutput
context.SyntaxProvider.CreateSyntaxProvider
Jetzt generieren und geben wir Code basierend auf den Daten aus, die von verarbeitet werden sollen.
context.SyntaxProvider.CreateSyntaxProvider
return
Der Wert von kann als zweites Argument von Action
empfangen werden, sodass der Code basierend auf diesem Wert generiert wird.
In diesem Fall erstellen wir Code, um die Syntaxliste der Eigenschaften aus der Syntax der Klasse abzurufen und die default
Eigenschaft aus jedem Namen auf zu setzen.
Erstellen Sie danach eine Methode basierend auf Reset
dem Klassennamen, betten Sie den Code der zuvor erstellten Eigenschaftenliste ein und setzen Sie den Wert default
aller Eigenschaften wieder Reset
auf Die Methode ist abgeschlossen.
Der Code wird contextSource.AddSource
für jede Klasse in der Methode separat ausgegeben.
Der Grund, warum ich "g" in den Dateinamen einfüge, ist übrigens, um leichter feststellen zu können, ob es sich um manuell erstellten Code oder einen automatisch generierten Codefehler handelt, wenn ein Build-Fehler auftritt.
Wenn Sie es tatsächlich tun, erhalten Sie Anfragen wie "Ich möchte das Feld zurücksetzen", "Ich möchte es anders als Standard initialisieren", "Ich möchte nur die automatischen Eigenschaften zurücksetzen". Wenn Sie sie einsetzen, wird das Kabel lang, also versuchen Sie, es selbst zu machen.
Anwendungsprojektseite
Dieses Mal habe ich einen Code erstellt, der die Methode der Klasse erweitert, also werde ich eine Klasse entsprechend erstellen.
Der Inhalt ist wie folgt, aber wenn Eigenschaften vorhanden sind, können Sie den Rest entsprechend verwenden.
Player.cs
public partial class Player
{
public string Name { get; set; } = "";
public float PositionX { get; set; }
public float PositionY { get; set; }
public int Level { get; set; } = 1;
public override string ToString()
{
return $"{nameof(Player)} {{{nameof(Name)} = {Name}, {nameof(PositionX)} = {PositionX}, {nameof(PositionY)} = {PositionY}, {nameof(Level)} = {Level}}}";
}
}
Item.cs
public partial class Item
{
public string Name { get; set; } = "";
public float PositionX { get; set; }
public float PositionY { get; set; }
public int Type { get; set; }
public override string ToString()
{
return $"{nameof(Item)} {{{nameof(Name)} = {Name}, {nameof(PositionX)} = {PositionX}, {nameof(PositionY)} = {PositionY}, {nameof(Type)} = {Type}}}";
}
}
Die Methode wird erweitert, wenn Reset
Sie den Code erstellen, aber sie wird möglicherweise nicht in Visual Studio widergespiegelt, also starten Sie Visual Studio zu diesem Zeitpunkt neu.
Ich denke, Sie können sehen, dass der Code automatisch auf den Analysator erweitert wird.
Die Einrückung ist seltsam, aber Sie können sie ignorieren, da Sie den automatisch generierten Code im Grunde nicht berühren.
Program.cs
Versuchen Sie, Code zu schreiben, um zu sehenReset
, ob Sie die Methode aufrufen können.
Ich denke, die Ergebnisse der Ausführung spiegeln sich wie erwartet wider.
Program.cs
Console.WriteLine("Hello, World!");
SampleClass.Hello();
Console.WriteLine();
Player player = new()
{
Name = "Mike",
PositionX = 10,
PositionY = 5.5f,
Level = 3,
};
Console.WriteLine(player);
player.Reset();
Console.WriteLine(player);
Console.WriteLine();
Item item = new()
{
Name = "Banana",
PositionX = 50,
PositionY = 53.5f,
Type = 12,
};
Console.WriteLine(item);
item.Reset();
Console.WriteLine(item);
Console.WriteLine();