Brug Visual Studio og Source Generator til automatisk at generere kode
Driftsmiljø
- Visual Studio
-
- Visual Studio 2022
- .NET
-
- .NET 8.0
Forudsætninger
- Visual Studio
-
- Det fungerer selv med en noget ældre version
- .NET
-
- Det fungerer selv med en noget ældre version
Først
Der er flere teknikker til automatisk generering af kode med dine egne definitioner, men i denne artikel vil jeg vise dig, hvordan du bruger en Source Generator. En af de største fordele ved en kildegenerator er, at den analyserer strukturen i det aktuelle projekts kildekode og genererer ny kode baseret på den. Når du f.eks. opretter en ny klasse, kan du gøre den, så koden automatisk tilføjes, så den svarer til klassen. Du kan programmere, hvilken slags kode du vil generere, så du kan oprette enhver form for automatisk kodegenerering, du kan lide.
Koden genereres i det væsentlige automatisk i baggrunden og indarbejdes i projektet bag kulisserne. Da det ikke udskrives som en fil på en synlig måde, bruges den ikke til at genbruge den automatisk genererede kode til generelle formål (selvom den kan fjernes ved at kopiere den indtil videre). Men da koden automatisk genereres i henhold til projektets struktur, reducerer den omkostningerne ved manuel indtastning og reducerer kodeskrivningsfejl i overensstemmelse hermed, hvilket er en enorm fordel.
I denne artikel vil jeg forklare, hvordan man kontrollerer, at koden genereres automatisk, så jeg vil ikke gå så langt som faktisk at analysere koden dybt og udføre avanceret output. Slå det selv op som en ansøgning.
Installationsprogrammet
Installer først Visual Studio. En kort forklaring er opsummeret i følgende tips.
Grundlæggende kan du bruge det til ethvert projekt, så det betyder ikke noget, hvilken arbejdsbyrde du konfigurerer. Men denne gang, som en "individuel komponent", ". NET Compiler Platform SDK. Dette er nyttigt til fejlfinding under udvikling af Source Generator. Hvis du allerede har Visual Studio installeret, kan du tilføje det fra Visual Studio-menuen under Værktøjer > Hent værktøjer og funktioner.
Oprettelse og forberedelse af et kildegeneratorprojekt
Source Generator oprettes i et projekt adskilt fra hovedapplikationsprojektet. Det betyder ikke noget, om du opretter dem først eller opretter yderligere senere. I dette tilfælde vil jeg oprette det fra Source Generator-projektet.
På skærmbilledet Opret nyt projekt skal du vælge Klassebibliotek.
Projektnavnet kan være hvad som helst, men indtil videre CodeGenerator
lader vi det være som .
For klassebiblioteker understøtter Source Generator i øjeblikket . NETTO Standard 2.0.
Når du har oprettet dit projekt, skal du hente pakken med Microsoft.CodeAnalysis.CSharp
NuGet.
Adfærden kan variere afhængigt af versionen, men der er ingen mening i at fortsætte med at bruge den gamle version, så jeg vil sætte den nyeste version.
Åbn derefter projektfilen som kode.
Når du åbner den, vil du se følgende.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
</ItemGroup>
</Project>
Tilføj det som følger: Du er velkommen til at tilføje alt andet, du har brug for.
<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>
Skriv derefter koden til den klasse, der automatisk genererer koden.
Først og fremmest vil vi kun oprette en ramme, så omskriv venligst den eksisterende kode fra begyndelsen Class1.cs
eller tilføj en ny kode.
Koden skal se sådan ud: Klassenavnet kan være hvad som helst, men det er bedre at have et navn, der viser, hvilken slags kode der automatisk genereres.
SampleGenerator
For nu, lad det være som .
Initialize
I denne metode skriver du kodeanalysen og kodegenereringsprocessen.
using Microsoft.CodeAnalysis;
namespace CodeGenerator
{
[Generator(LanguageNames.CSharp)]
public partial class SampleGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
}
}
}
Opret derefter et projekt til det program, som du automatisk vil generere kode for. Det kan være hvad som helst, men her vil vi simpelthen bruge det som en konsolapplikation. Typen og versionen af rammen for projektet, som koden automatisk genereres til, svarer til det generelle nummer.
Føj en reference til kildegeneratorprojektet fra projektet på programsiden.
Nogle indstillinger kan ikke indstilles i egenskaberne, så åbn projektfilen i kode.
Jeg synes, det ser sådan ud:
<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>
Tilføj indstillingerne for det refererede projekt på følgende måde: Sørg for, at du ikke laver en fejl i XML-filens syntaks.
<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>
Åbn derefter projektegenskaberne på "Code Generator-siden".
Klik på linket for at åbne brugergrænsefladen til fejlfindingsstartegenskaber.
Slet den oprindelige profil, fordi du ikke vil bruge den.
Tilføj en ny profil.
Vælg Roslyn-komponent.
Hvis du har foretaget indstillingerne indtil videre, skal du være i stand til at vælge applikationsprojektet, så vælg det.
Dette er slutningen af forberedelsen.
Kontroller, om du kan fejlfinde
Åbn kildekoden, og Initialize
placer et brudpunkt i slutningen af metoden.
Lad os fejlfinde kildegeneratoren.
Hvis processen stopper ved brudpunktet, kan du bekræfte, at du fejler normalt. Dette skulle gøre udviklingen af din kildegenerator rimelig let.
Lad os indtil videre udsende en fast kode
Lad os først prøve at udsende en fast kode let. Det er nemt, fordi du ikke engang behøver at analysere koden. Selvom det er en fast kode, håndteres den som en streng, så det er muligt at øge produktionen af kode i en fast form ved at opbygge et program.
Hvis du vil udskrive en fast kode, kan du context.RegisterPostInitializationOutput
gøre det ved hjælp af .
Følgende er et eksempel på kodeoutputtet.
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));
});
}
}
Indholdet af koden er som skrevet i kommentarerne, så jeg vil udelade detaljerne. Byg det, og sørg for, at der ikke er nogen fejl.
Når bygningen er færdig, kan du udvide "Analysatorer" i applikationsprojektet for at se koden genereret af kodegeneratoren.
Hvis du ikke kan se det, skal du genstarte Visual Studio og kontrollere det. Det ser ud til, at Visual Studio på nuværende tidspunkt muligvis ikke opdateres, medmindre du genstarter det. Den Intellisence- og syntaksfremhævning, der bruges ved oprettelse af programmer, er ens. Men da selve koden afspejles på tidspunktet for opbygningen, ser det ud til, at programmet afspejles på applikationssiden.
Når koden er genereret, kan du prøve at bruge den i din applikation. Det skal fungere fint.
Analysér og generer kode
Hvis du bare behandler koden normalt og udsender den, er den ikke meget forskellig fra anden automatisk kodegenerering, og du kan ikke drage fordel af fordelene ved kildegeneratoren. Så lad os nu analysere koden for applikationsprojektet og generere koden i overensstemmelse hermed.
Analysen af koden er dog ret dyb, så jeg kan ikke forklare alt her. Indtil videre vil jeg forklare op til det punkt, hvor du kan analysere og output koden. Hvis du vil gå dybere, skal du lave din egen research.
For øjeblikket vil jeg som et eksempel automatisk oprette koden "tilføj en metode til Reset
alle oprettede klasser og nulstil værdien default
af alle egenskaber til ".
I de senere år ser det ud til, at håndtering af uforanderlige forekomster er blevet foretrukket, men i spilprogrammer er det ikke muligt at oprette en ny forekomst hver ramme, så jeg tror, at der er brug for processen med at nulstille værdien.
default
Du vil måske indstille noget andet end det, men hvis du gør det, vil det være et langt første eksempel, så anvend det selv.
Kode Generator Side
Opret en ny klasse med det formål at beholde den generator, du oprettede før. Hvis indholdet, der skal genereres automatisk, ændres, er det bedre at oprette en ny i en anden klasse.
Roslyn-kompilatoren tager sig af struktureringen af koden, så hvad vi skal gøre her er at analysere de strukturerede data og skrive koden.
Først sender jeg al koden. Jeg har forsøgt at have så lidt kode som muligt. Denne gang fungerer det, men det er kun på et niveau, der bevæger sig som en Mr./Ms., så når du rent faktisk går videre med udviklingen, vil der mangle en hel del dele. Forbedr venligst der efter behov.
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);
});
}
}
Jeg har skrevet om det i kommentarerne, men jeg vil forklare det nogle steder.
// 構文を解析し処理対象のノードをコード出力用に変換します
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: ...,
// 対象のノードをコード出力に必要な形に変換します
transform: ...
);
context.SyntaxProvider.CreateSyntaxProvider
at analysere koden i applikationsprojektet og strukturere den så meget som muligt.
Alt er opdelt i noder, og vi bruger til at bestemme, hvilken af dem vi predicate
vil behandle.
Også, hvis det er nødvendigt transform
, konvertere det behandlede objekt med og sende det til outputsiden.
predicate
I dette tilfælde gør vi følgende.
// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: static (node, cancelToken) =>
{
// 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
cancelToken.ThrowIfCancellationRequested();
// クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
// また partial class であること
return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},
Først og fremmest, som med enhver proces, kan processen med automatisk generering af kode altid annulleres for tidligt.
Så brug annulleringstoken til at ThrowIfCancellationRequested
ringe, så du når som helst kan afbryde.
predicate
Nu hvor hver knude kaldes, vil vi bestemme, hvilken der er den, vi vil behandle.
Da der er et stort antal af dem, er det bedre at indsnævre dem her til en vis grad.
Da vi vil tilføje behandling til klassen denne gang, vil vi ClassDeclarationSyntax
afgøre, om det er og afgøre, om kun klassen vil blive behandlet.
partial class
Da koden er knyttet til , sættes den partial class
også som en dom.
// 対象のノードをコード出力に必要な形に変換します
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
Giver dig mulighed for at konvertere analysen til den ønskede form og videregive den til kodeudgangen.
Denne gang foretager vi ikke meget konvertering, men overfører værdien til outputsiden, som den er return
.
Vi får "erklærede symboler" undervejs, menSemanticModel
vi kan få en masse yderligere oplysninger ved hjælp af og , Syntac
så jeg tror, vi kan få det, hvis det er nødvendigt.
return
Tupler oprettes og returneres, men konfigurationen af de data, der skal sendes til outputsiden, kan være hvad som helst.
Hvis du vil videregive flere data, kan du oprette en tuple som denne, eller du kan definere din egen klasse og videregive den.
// 解析し変換した情報をもとにコードを出力します
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
Nu genererer og udsender vi kode baseret på de data, der skal behandles af .
context.SyntaxProvider.CreateSyntaxProvider
return
Værdien af kan modtages som det andet argument Action
for , så koden genereres baseret på denne værdi.
I dette tilfælde opretter vi kode for at hente syntakslisten over egenskaber fra klassens syntaks og indstille egenskaben default
til fra hvert navn.
Derefter skal du oprette en metode baseret på Reset
klassenavnet, integrere koden for egenskabslisten, der blev oprettet tidligere, og indstille værdien default
af alle egenskaber tilbage Reset
til Metoden er afsluttet.
Koden contextSource.AddSource
udsendes separat for hver klasse i metoden.
Forresten er grunden til, at jeg sætter "g" i filnavnet, at gøre det lettere at afgøre, om det er manuelt oprettet kode eller automatisk genereret kodefejl, når der er en build-fejl.
Hvis du rent faktisk gør det, vil du modtage anmodninger som "Jeg vil nulstille feltet", "Jeg vil initialisere det andet end standard", "Jeg vil kun nulstille de automatiske egenskaber". Hvis du sætter dem i, vil ledningen være lang, så prøv at lave det selv.
Ansøgning projekt side
Denne gang oprettede jeg en kode, der udvider klassens metode, så jeg vil oprette en klasse korrekt.
Indholdet er som følger, men hvis der er egenskaber, kan du bruge resten efter behov.
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}}}";
}
}
Metoden udvides, når Reset
du opretter koden, men den afspejles muligvis ikke i Visual Studio, så genstart Visual Studio på det tidspunkt.
Jeg tror, du kan se, at koden automatisk udvides til analysatoren.
Indrykningen er mærkelig, men du kan ignorere den, fordi du dybest set ikke rører ved den automatisk genererede kode.
Program.cs
Prøv at skrive kode ind for at se, Reset
om du kan kalde metoden.
Jeg tror, at resultaterne af udførelsen afspejles som forventet.
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();