Använd Visual Studio och Source Generator för att automatiskt generera kod

Sidan uppdaterad :
Datum för skapande av sida :

Omvärld

Visuell studio
  • Visual Studio 2022
.NÄT
  • .NET 8.0

Förutsättningar

Visuell studio
  • Det fungerar även med en något äldre version
.NÄT
  • Det fungerar även med en något äldre version

Först

Det finns flera tekniker för att automatiskt generera kod med dina egna definitioner, men i den här artikeln visar jag dig hur du använder en källgenerator. En av de största fördelarna med en källgenerator är att den analyserar strukturen i det aktuella projektets källkod och genererar ny kod baserat på den. När du till exempel skapar en ny klass kan du göra så att koden automatiskt läggs till för att matcha klassen. Du kan programmera vilken typ av kod du vill generera, så att du kan skapa vilken form av automatisk kodgenerering du vill.

Koden genereras i huvudsak automatiskt i bakgrunden och införlivas i projektet bakom kulisserna. Eftersom den inte matas ut som en fil på ett synligt sätt, används den inte i syfte att återanvända den automatiskt genererade koden för allmänna ändamål (även om den för närvarande kan tas bort genom att kopiera den). Men eftersom koden genereras automatiskt enligt projektets struktur minskar det kostnaden för manuell inmatning och minskar kodskrivningsfel i enlighet med detta, vilket är en stor fördel.

I den här artikeln kommer jag att förklara hur man kontrollerar att koden genereras automatiskt, så jag kommer inte att gå så långt som att faktiskt analysera koden djupt och utföra avancerade utdata. Slå upp det själv som en ansökan.

Installationen

Installera först Visual Studio. En kort förklaring sammanfattas i följande tips.

I grund och botten kan du använda den för vilket projekt som helst, så det spelar ingen roll vilken arbetsbelastning du ställer in. Men den här gången, som en "enskild komponent", ". SDK för NET-kompilatorplattformen. Detta är användbart för felsökning under utvecklingen av källgeneratorn. Om du redan har Visual Studio installerat kan du lägga till det från Visual Studio-menyn under Verktyg > Hämta verktyg och funktioner.

Skapa och förbereda ett källgeneratorprojekt

Källgeneratorn skapas i ett projekt som är separat från huvudprogramprojektet. Det spelar ingen roll om du skapar dem först eller skapar ytterligare senare. I det här fallet kommer jag att skapa den från Source Generator-projektet.

På skärmen Skapa nytt projekt väljer du Klassbibliotek.

Projektnamnet kan vara vad som helst, men för tillfället CodeGenerator lämnar vi det som .

För klassbibliotek har Source Generator för närvarande stöd för . NET Standard 2.0.

När du har skapat projektet hämtar du paketet med Microsoft.CodeAnalysis.CSharp NuGet. Beteendet kan skilja sig åt beroende på version, men det är ingen idé att fortsätta använda den gamla versionen, så jag kommer att lägga den senaste versionen.

Öppna sedan projektfilen som kod.

När du öppnar den kommer du att se följande.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
  </ItemGroup>

</Project>

Lägg till det enligt följande: Lägg gärna till något annat du behöver.

<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 sedan koden för klassen som automatiskt genererar koden. Först och främst kommer vi bara att skapa ett ramverk, så skriv om den befintliga koden från början Class1.cs eller lägg till en ny kod.

Koden ska se ut så här: Klassnamnet kan vara vad som helst, men det är bättre att ha ett namn som visar vilken typ av kod som genereras automatiskt. SampleGenerator För tillfället lämnar du det som . Initialize I den här metoden kommer du att skriva kodanalysen och kodgenereringsprocessen.

using Microsoft.CodeAnalysis;

namespace CodeGenerator
{
  [Generator(LanguageNames.CSharp)]
  public partial class SampleGenerator : IIncrementalGenerator
  {
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
    }
  }
}

Skapa sedan ett projekt för det program som du vill generera kod för automatiskt. Det kan vara vad som helst, men här kommer vi helt enkelt att använda det som en konsolapplikation. Typen och versionen av ramverket för det projekt som koden genereras till automatiskt motsvarar det allmänna numret.

Lägg till en referens till källgeneratorprojektet från projektet på programsidan.

Vissa inställningar kan inte anges i egenskaperna, så öppna projektfilen i kod.

Jag tror att det ser ut så här:

<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>

Lägg till inställningarna för det refererade projektet på följande sätt: Se till att du inte gör något misstag i XML-syntaxen.

<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>

Öppna sedan projektegenskaperna på sidan "Kodgenerator".

Klicka på länken för att öppna användargränssnittet för egenskaper för felsökningsstart.

Ta bort den ursprungliga profilen eftersom du inte vill använda den.

Lägg till en ny profil.

Välj Roslyn-komponent.

Om du har gjort inställningarna hittills bör du kunna välja applikationsprojekt, så välj det.

Detta är slutet på förberedelserna.

Kontrollera om du kan felsöka

Öppna källkoden för generatorn och Initialize placera en brytpunkt i slutet av metoden.

Nu ska vi felsöka källgeneratorn.

Om processen stoppas vid brytpunkten kan du bekräfta att du felsöker normalt. Detta bör göra utvecklingen av din källgenerator ganska enkel.

För tillfället, låt oss mata ut en fast kod

Låt oss först försöka mata ut en fast kod enkelt. Det är enkelt eftersom du inte ens behöver analysera koden. Även om det är en fast kod hanteras den som en sträng, så det går att öka produktionen av kod i fast form genom att bygga ett program.

Om du vill mata ut en fast kod kan du context.RegisterPostInitializationOutput göra det med hjälp av . Följande är ett exempel på kodutdata.

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));
    });
  }
}

Innehållet i koden är som skrivet i kommentarerna, så jag kommer att utelämna detaljerna. Bygg den och se till att det inte finns några fel.

När bygget är klart kan du expandera "Analyzers" i applikationsprojektet för att se koden som genereras av kodgeneratorn.

Om du inte ser den startar du om Visual Studio och kontrollerar den. Det verkar som om Visual Studio för närvarande kanske inte uppdateras om du inte startar om det. Intelligensen och syntaxmarkeringen som används när man skapar program är likartad. Men eftersom själva koden återspeglas vid tidpunkten för bygget, verkar det som om programmet återspeglas på applikationssidan.

När koden har genererats kan du prova att använda den i ditt program. Det borde fungera bra.

Analysera och generera kod

Om du bara bearbetar koden normalt och matar ut den skiljer den sig inte mycket från annan automatisk kodgenerering, och du kan inte dra nytta av fördelarna med källgeneratorn. Så låt oss nu analysera koden för applikationsprojektet och generera koden därefter.

Analysen av koden är dock ganska djup, så jag kan inte förklara allt här. För närvarande kommer jag att förklara fram till den punkt där du kan analysera och mata ut koden. Om du vill gå djupare, gör din egen forskning.

För tillfället, som ett exempel, skulle jag vilja automatiskt skapa koden "lägg till en metod till Reset alla skapade klasser och återställ värdet default för alla egenskaper till ". Under de senaste åren verkar det som om man har föredragit att hantera oföränderliga instanser, men i spelprogram är det inte möjligt att skapa en ny instans varje bildruta, så jag tror att det finns en användning för processen att återställa värdet. default Du kanske vill ställa in något annat än det, men om du gör det kommer det att bli ett långt första exempel, så använd det själv.

Kodgeneratorns sida

Skapa en ny klass i syfte att behålla generatorn som du skapade tidigare. Om innehållet som ska genereras automatiskt ändras är det bättre att skapa ett nytt i en annan klass.

Roslyn-kompilatorn tar hand om struktureringen av koden, så vad vi ska göra här är att parsa strukturerade data och skriva koden.

Först ska jag lägga upp all kod. Jag har försökt att ha så lite kod som möjligt. Den här gången fungerar det, men det är bara på en nivå som rör sig som en Mr/Ms., så när du faktiskt går vidare med utvecklingen kommer det att saknas en hel del delar. Var snäll och förbättra där 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);
    });
  }
}

Jag har skrivit om det i kommentarerna, men jag ska förklara det på några ställen.

// 構文を解析し処理対象のノードをコード出力用に変換します
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
  // すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
  predicate: ...,
  // 対象のノードをコード出力に必要な形に変換します
  transform: ...
);

context.SyntaxProvider.CreateSyntaxProvider att analysera koden i applikationsprojektet och strukturera den så mycket som möjligt. Allt är uppdelat i noder, och vi använder för att bestämma vilken av dem vi predicate vill bearbeta. transform Konvertera också det bearbetade objektet med och skicka det till utdatasidan.

predicate I det här fallet gör vi följande.

// すべてのノードが処理対象となるため、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 och främst, som med alla processer, kan processen att automatiskt generera kod alltid avbrytas i förtid. Så använd annulleringstoken för att ThrowIfCancellationRequested anropa så att du kan avbryta när som helst.

predicate Nu när varje nod anropas vill vi avgöra vilken som är den vi vill bearbeta. Eftersom det finns ett stort antal av dem är det bättre att begränsa dem här i viss utsträckning.

Eftersom vi ska lägga till bearbetning i klassen den här gången avgör vi ClassDeclarationSyntax om det är det och avgör om endast klassen ska bearbetas. partial class Dessutom, eftersom koden är bifogad med , sätts den partial class 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 Gör att du kan konvertera analysen till det formulär som krävs och skicka den till kodutdata. Den här gången gör vi inte mycket konvertering, utan skickar värdet till utdatasidan som det är return . Vi får "deklarerade symboler" längs vägen, menSemanticModel vi kan få en hel del ytterligare information med hjälp av och , Syntac så jag tror att vi kan få det om det behövs. return Tupplar skapas och returneras, men konfigurationen av de data som ska skickas till utdatasidan kan vara vad som helst. Om du vill skicka flera data kan du skapa en tuppeln så här, eller så kan du definiera en egen klass och skicka in 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.RegisterSourceOutputcontext.SyntaxProvider.CreateSyntaxProvider Nu kommer vi att generera och mata ut kod baserat på de data som ska bearbetas av . context.SyntaxProvider.CreateSyntaxProvider return Värdet för kan tas emot som det andra argumentet Action för , så koden genereras baserat på det värdet.

I det här fallet skapar vi kod för att hämta syntaxlistan med egenskaper från klassens syntax och ställa in default egenskapen till från varje namn.

Därefter skapar du en metod baserat på Reset klassnamnet, bäddar in koden för egenskapslistan som skapades tidigare och anger värdet default för alla egenskaper tillbaka Reset till Metoden är slutförd.

Koden matas contextSource.AddSource ut separat för varje klass i metoden. Förresten, anledningen till att jag lägger "g" i filnamnet är för att göra det lättare att avgöra om det är manuellt skapad kod eller automatiskt genererat kodfel när det finns ett byggfel.

Om du faktiskt gör det kommer du att få förfrågningar som "Jag vill återställa fältet", "Jag vill initiera det på annat sätt än standard", "Jag vill bara återställa de automatiska egenskaperna". Om du sätter i dem blir sladden lång, så försök att göra den själv.

Ansökan projektsida

Den här gången skapade jag en kod som utökar klassens metod, så jag kommer att skapa en klass på lämpligt sätt.

Innehållet är följande, men om det finns egenskaper kan du använda 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 utökas när Reset du skapar koden, men den kanske inte återspeglas i Visual Studio, så starta om Visual Studio vid den tidpunkten. Jag tror att du kan se att koden automatiskt utökas till analysatorn.

Indraget är konstigt, men du kan ignorera det eftersom du i princip inte rör den automatiskt genererade koden.

Program.cs Prova att skriva in kod för att se Reset om du kan anropa metoden. Jag tror att resultatet av utförandet återspeglas som förväntat.

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();