Gebruik Visual Studio en Source Generator om automatisch code te genereren

Pagina bijgewerkt :
Aanmaakdatum van pagina :

Werkomgeving

Visual Studio
  • Visual Studio 2022
.NET
  • .NET 8.0

Voorwaarden

Visual Studio
  • Het werkt zelfs met een wat oudere versie
.NET
  • Het werkt zelfs met een wat oudere versie

Eerst

Er zijn verschillende technieken om automatisch code te genereren met je eigen definities, maar in dit artikel laat ik je zien hoe je een Source Generator gebruikt. Een van de grootste voordelen van een brongenerator is dat deze de structuur van de broncode van het huidige project analyseert en op basis daarvan nieuwe code genereert. Als je bijvoorbeeld een nieuwe lesgroep maakt, kun je ervoor zorgen dat de code automatisch wordt toegevoegd om overeen te komen met de lesgroep. U kunt programmeren wat voor soort code u wilt genereren, zodat u elke vorm van automatische codegeneratie kunt maken die u maar wilt.

De code wordt in wezen automatisch op de achtergrond gegenereerd en achter de schermen in het project opgenomen. Omdat het niet op een zichtbare manier als bestand wordt uitgevoerd, wordt het niet gebruikt voor het hergebruik van de automatisch gegenereerde code voor algemene doeleinden (hoewel het voorlopig kan worden verwijderd door het te kopiëren). Omdat de code echter automatisch wordt gegenereerd volgens de structuur van het project, verlaagt het de kosten van handmatige invoer en vermindert het dienovereenkomstig het schrijven van code, wat een enorm voordeel is.

In dit artikel zal ik uitleggen hoe je kunt controleren of de code automatisch wordt gegenereerd, dus ik zal niet zo ver gaan om de code daadwerkelijk diepgaand te analyseren en geavanceerde uitvoer uit te voeren. Zoek het zelf op als applicatie.

Setup

Installeer eerst Visual Studio. Een korte uitleg is samengevat in de volgende Tips.

In principe kun je het voor elk project gebruiken, dus het maakt niet uit welke workload je instelt. Echter, deze keer, als een "individueel onderdeel", ". SDK van het NET-compilerplatform. Dit is handig voor foutopsporing tijdens de ontwikkeling van Source Generator. Als u Visual Studio al hebt geïnstalleerd, kunt u het toevoegen via het menu Visual Studio onder Tools > Tools en functies downloaden.

Een brongeneratorproject maken en voorbereiden

Source Generator wordt gemaakt in een project dat losstaat van het hoofdtoepassingsproject. Het maakt niet uit of u ze eerst maakt of later extra maakt. In dit geval zal ik het maken vanuit het Source Generator-project.

Selecteer in het scherm Nieuw project maken de optie Klasbibliotheek.

De projectnaam kan van alles zijn, maar voor nu CodeGenerator laten we het bij .

Voor klassenbibliotheken ondersteunt Source Generator momenteel . NET-standaard 2.0.

Zodra u uw project hebt gemaakt, krijgt u het pakket met Microsoft.CodeAnalysis.CSharp NuGet. Het gedrag kan verschillen afhankelijk van de versie, maar het heeft geen zin om de oude versie te blijven gebruiken, dus ik zal de nieuwste versie plaatsen.

Open vervolgens het projectbestand als code.

Wanneer u het opent, ziet u het volgende.

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

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

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

</Project>

Voeg het als volgt toe: Voel je vrij om al het andere toe te voegen dat je nodig hebt.

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

Schrijf vervolgens de code voor de klasse die de code automatisch genereert. Allereerst zullen we alleen een raamwerk maken, dus herschrijf de bestaande code vanaf het begin Class1.cs of voeg een nieuwe code toe.

De code zou er als volgt uit moeten zien: De klassenaam kan van alles zijn, maar het is beter om een naam te hebben die laat zien wat voor soort code automatisch wordt gegenereerd. SampleGenerator Laat het voorlopig op . Initialize In deze methode schrijf je het code-analyse- en codegeneratieproces.

using Microsoft.CodeAnalysis;

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

Maak vervolgens een project aan voor de toepassing waarvoor u automatisch code wilt genereren. Het kan van alles zijn, maar hier gebruiken we het gewoon als een console-applicatie. Het type en de versie van het raamwerk van het project waarvoor de code automatisch wordt gegenereerd, komt overeen met het algemene nummer.

Voeg een verwijzing toe naar het brongeneratorproject van het project aan de toepassingszijde.

Sommige instellingen kunnen niet worden ingesteld in de eigenschappen, dus open het projectbestand in code.

Ik denk dat het er zo uitziet:

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

Voeg de instellingen voor het project waarnaar wordt verwezen als volgt toe: Zorg ervoor dat u geen fout maakt in de syntaxis van de XML.

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

Open vervolgens de projecteigenschappen aan de kant van de "Code Generator".

Klik op de koppeling om de gebruikersinterface voor starteigenschappen voor foutopsporing te openen.

Verwijder het oorspronkelijke profiel omdat u het niet wilt gebruiken.

Voeg een nieuw profiel toe.

Selecteer Roslyn-component.

Als u de instellingen tot nu toe hebt gemaakt, zou u het toepassingsproject moeten kunnen selecteren, dus selecteer het.

Dit is het einde van de voorbereiding.

Controleer of u fouten kunt opsporen

Open de code van de brongenerator en Initialize plaats een onderbrekingspunt aan het einde van de methode.

Laten we de brongenerator debuggen.

Als het proces stopt bij het onderbrekingspunt, kunt u bevestigen dat u normaal foutopsporing uitvoert. Dit zou de ontwikkeling van uw brongenerator redelijk eenvoudig moeten maken.

Laten we voorlopig een vaste code uitvoeren

Laten we eerst proberen om gemakkelijk een vaste code uit te voeren. Het is gemakkelijk omdat u de code niet eens hoeft te analyseren. Zelfs als het een vaste code is, wordt het behandeld als een string, dus het is mogelijk om de productie van code in een vaste vorm te verhogen door een programma te bouwen.

Als u een vaste code wilt uitvoeren, kunt u context.RegisterPostInitializationOutput dit doen met behulp van . Het volgende is een voorbeeld van de code-uitvoer.

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

De inhoud van de code is zoals beschreven in de opmerkingen, dus ik zal de details weglaten. Bouw het en zorg ervoor dat er geen fouten zijn.

Wanneer de build is voltooid, kunt u "Analyzers" in het toepassingsproject uitvouwen om de code te zien die door de codegenerator is gegenereerd.

Als u het niet ziet, start u Visual Studio opnieuw en controleert u het. Het lijkt erop dat Visual Studio op dit moment mogelijk niet wordt bijgewerkt, tenzij u het opnieuw start. De Intellisence en syntaxismarkering die worden gebruikt bij het maken van programma's zijn vergelijkbaar. Aangezien de code zelf echter wordt weerspiegeld op het moment van bouwen, lijkt het erop dat het programma wordt weerspiegeld aan de applicatiekant.

Zodra de code is gegenereerd, kunt u deze in uw toepassing gebruiken. Het zou goed moeten werken.

Analyseer en genereer code

Als u de code gewoon normaal verwerkt en uitvoert, verschilt deze niet veel van andere automatische codegeneratie en kunt u niet profiteren van de voordelen van de brongenerator. Laten we nu de code van het applicatieproject analyseren en de code dienovereenkomstig genereren.

De analyse van de code is echter vrij diepgaand, dus ik kan hier niet alles uitleggen. Voorlopig zal ik het uitleggen tot het punt waarop je de code kunt analyseren en uitvoeren. Als je dieper wilt gaan, doe dan je eigen onderzoek.

Voorlopig zou ik als voorbeeld automatisch de code willen maken "voeg een methode toe aan Reset alle aangemaakte klassen en reset de waarde default van alle eigenschappen naar ". In de afgelopen jaren lijkt het erop dat het afhandelen van onveranderlijke instanties de voorkeur heeft gekregen, maar in spelprogramma's is het niet mogelijk om elk frame een nieuwe instantie te maken, dus ik denk dat er een nut is voor het proces van het resetten van de waarde. default Misschien wil je iets anders instellen dan dat, maar als je dat doet, wordt het een lang eerste voorbeeld, dus pas het zelf toe.

Kant van de codegenerator

Maak een nieuwe klasse met als doel de generator die je eerder hebt gemaakt te behouden. Als de inhoud die automatisch moet worden gegenereerd verandert, is het beter om een nieuwe te maken in een andere klas.

De Roslyn-compiler zorgt voor de structurering van de code, dus wat we hier gaan doen is de gestructureerde gegevens ontleden en de code schrijven.

Eerst zal ik alle code posten. Ik heb geprobeerd zo min mogelijk code te hebben. Deze keer werkt het, maar het is alleen op een niveau dat beweegt als een meneer/mevrouw, dus als je daadwerkelijk doorgaat met ontwikkelen, zullen er nogal wat ontbrekende onderdelen zijn. Verbeter daar indien nodig.

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

Ik heb erover geschreven in de reacties, maar ik zal het op sommige plaatsen uitleggen.

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

context.SyntaxProvider.CreateSyntaxProvider om de code in het applicatieproject te analyseren en zoveel mogelijk te structureren. Alles is opgedeeld in knooppunten, en we gebruiken het om te bepalen welke we predicate willen verwerken. Converteer ook, indien nodig transform , het verwerkte object met en geef het door aan de uitvoerzijde.

predicate In dit geval doen we het volgende.

// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: static (node, cancelToken) =>
{
  // 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
  cancelToken.ThrowIfCancellationRequested();

  // クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
  // また partial class であること
  return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},

Allereerst kan, zoals bij elk proces, het proces van het automatisch genereren van code altijd voortijdig worden geannuleerd. Gebruik dus het annuleringstoken om te ThrowIfCancellationRequested bellen, zodat u op elk moment kunt onderbreken.

predicate Nu elke node is aangeroepen, willen we bepalen welke we willen verwerken. Aangezien het er enorm veel zijn, is het beter om ze hier tot op zekere hoogte te beperken.

Aangezien we deze keer verwerking aan de klasse gaan toevoegen, zullen we ClassDeclarationSyntax bepalen of dit het geval is en bepalen of alleen de klasse wordt verwerkt. partial class Omdat de code is bijgevoegd met , wordt het partial class ook als een oordeel geplaatst.

// 対象のノードをコード出力に必要な形に変換します
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 Hiermee kunt u de analyse omzetten in de gewenste vorm en doorgeven aan de code-uitvoer. Deze keer doen we niet veel conversie, maar geven we de waarde door aan de uitvoerzijde zoals deze is return . We krijgen onderweg "verklaarde symbolen", maarSemanticModel we kunnen veel aanvullende informatie krijgen met behulp van en Syntac , dus ik denk dat we het kunnen krijgen als dat nodig is. return Tupels worden gemaakt en geretourneerd, maar de configuratie van de gegevens die aan de uitvoerzijde moeten worden doorgegeven, kan van alles zijn. Als je meerdere gegevens wilt doorgeven, kun je zo'n tuple maken, of je kunt je eigen klasse definiëren en doorgeven.

// 解析し変換した情報をもとにコードを出力します
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 zullen we code genereren en uitvoeren op basis van de gegevens die moeten worden verwerkt door . context.SyntaxProvider.CreateSyntaxProvider return De waarde van kan worden ontvangen als het tweede argument van , dus de code wordt gegenereerd op basis van Action die waarde.

In dit geval maken we code om de syntaxislijst met eigenschappen uit de syntaxis van de klasse te halen en de default eigenschap in te stellen op van elke naam.

Maak daarna een methode op Reset basis van de klassenaam, sluit de code in van de eerder gemaakte eigenschappenlijst en stel de waarde default van alle eigenschappen weer in Reset op De methode is voltooid.

De code wordt contextSource.AddSource voor elke klasse in de methode afzonderlijk uitgevoerd. Trouwens, de reden waarom ik "g" in de bestandsnaam heb gezet, is om het gemakkelijker te maken om te bepalen of het handmatig gemaakte code is of een automatisch gegenereerde codefout wanneer er een buildfout is.

Als je het daadwerkelijk haalt, krijg je verzoeken als "Ik wil het veld resetten", "Ik wil het anders initialiseren dan standaard", "Ik wil alleen de automatische eigenschappen resetten". Als je ze erin doet, wordt het snoer lang, dus probeer het zelf te maken.

Applicatie projectzijde

Deze keer heb ik een code gemaakt die de methode van de klasse uitbreidt, dus ik zal op de juiste manier een klasse maken.

De inhoud is als volgt, maar als er eigenschappen zijn, kunt u de rest naar wens gebruiken.

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

De methode wordt uitgebreid wanneer Reset u de code maakt, maar wordt mogelijk niet weergegeven in Visual Studio, dus start Visual Studio op dat moment opnieuw. Ik denk dat je kunt zien dat de code automatisch wordt doorgetrokken naar de analyzer.

De inspringing is vreemd, maar je kunt het negeren omdat je de automatisch gegenereerde code in principe niet aanraakt.

Program.cs Probeer code in te schrijven om te zien Reset of u de methode kunt aanroepen. Ik denk dat de resultaten van de uitvoering worden weerspiegeld zoals verwacht.

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