Použite Visual Studio a Source Generator na automatické generovanie kódu

Stránka aktualizovaná :
Dátum vytvorenia strany :

Prevádzkové prostredie

Vizuálne štúdio
  • Visual Studio 2022
.SIEŤ
  • .NET 8.0

Predpoklady

Vizuálne štúdio
  • Funguje to aj s trochu staršou verziou
.SIEŤ
  • Funguje to aj s trochu staršou verziou

Najskôr

Existuje niekoľko techník na automatické generovanie kódu s vlastnými definíciami, ale v tomto článku vám ukážem, ako používať zdrojový generátor. Jednou z najväčších výhod zdrojového generátora je, že analyzuje štruktúru zdrojového kódu aktuálneho projektu a na jeho základe generuje nový kód. Keď napríklad vytvoríte novú triedu, môžete ju nastaviť tak, aby sa kód automaticky pridal tak, aby zodpovedal triede. Môžete naprogramovať, aký druh kódu chcete generovať, aby ste mohli vytvoriť ľubovoľnú formu automatického generovania kódu, ktorá sa vám páči.

Kód je v podstate automaticky generovaný na pozadí a začlenený do projektu v zákulisí. Keďže nie je viditeľným spôsobom na výstupe ako súbor, nepoužíva sa na účely opätovného použitia automaticky vygenerovaného kódu na všeobecné účely (aj keď ho možno zatiaľ odstrániť jeho kopírovaním). Keďže sa však kód generuje automaticky podľa štruktúry projektu, znižuje náklady na manuálne zadávanie a zodpovedajúcim spôsobom znižuje chyby pri písaní kódu, čo je obrovská výhoda.

V tomto článku vysvetlím, ako skontrolovať, či sa kód generuje automaticky, takže nepôjdem tak ďaleko, aby som kód skutočne hlboko analyzoval a vykonal pokročilý výstup. Vyhľadajte si to sami ako aplikáciu.

nastavenie

Najprv nainštalujte Visual Studio. Stručné vysvetlenie je zhrnuté v nasledujúcich tipoch.

V podstate ho môžete použiť na akýkoľvek projekt, takže nezáleží na tom, aké pracovné zaťaženie nastavíte. Tentoraz však ako "samostatná zložka", ". SDK platformy kompilátora NET. To je užitočné pre ladenie počas vývoja generátora zdrojov. Ak už máte Visual Studio nainštalované, môžete ho pridať z ponuky Visual Studio v časti Nástroje > Získať nástroje a súčasti.

Vytvorenie a príprava projektu generátora zdrojov

Zdrojový generátor je vytvorený v projekte oddelenom od hlavného aplikačného projektu. Nezáleží na tom, či ich najprv vytvoríte alebo vytvoríte ďalšie neskôr. V tomto prípade ho vytvorím z projektu Source Generator.

Na obrazovke Vytvorenie nového projektu vyberte položku Knižnica tried.

Názov projektu môže byť akýkoľvek, ale zatiaľ CodeGenerator ho necháme ako .

Pre knižnice tried Generátor zdrojov momentálne podporuje . NET štandard 2.0.

Po vytvorení projektu získajte balík s Microsoft.CodeAnalysis.CSharp NuGet. Správanie sa môže líšiť v závislosti od verzie, ale nemá zmysel pokračovať v používaní starej verzie, preto dám najnovšiu verziu.

Potom otvorte súbor projektu ako kód.

Keď ho otvoríte, uvidíte nasledujúce.

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

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

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

</Project>

Doplňte ho takto: Neváhajte pridať čokoľvek ďalšie, čo potrebujete.

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

Potom napíšte kód pre triedu, ktorý automaticky vygeneruje kód. V prvom rade vytvoríme iba framework, preto prosím prepíšte existujúci kód od začiatku Class1.cs alebo pridajte nový.

Kód by mal vyzerať takto: Názov triedy môže byť čokoľvek, ale je lepšie mať názov, ktorý ukazuje, aký druh kódu sa generuje automaticky. SampleGenerator Zatiaľ to nechajte ako . Initialize V tejto metóde napíšete proces analýzy kódu a generovania kódu.

using Microsoft.CodeAnalysis;

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

Potom vytvorte projekt pre aplikáciu, pre ktorú chcete automaticky vygenerovať kód. Môže to byť čokoľvek, ale tu ho jednoducho použijeme ako konzolovú aplikáciu. Typ a verzia rámca projektu, do ktorého sa kód automaticky generuje, zodpovedá všeobecnému číslu.

Pridajte odkaz na projekt zdrojového generátora z projektu na strane aplikácie.

Niektoré nastavenia nie je možné nastaviť vo vlastnostiach, preto otvorte súbor projektu v kóde.

Myslím, že to vyzerá takto:

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

Nastavenia odkazovaného projektu pridajte takto: Uistite sa, že ste neurobili chybu v syntaxi 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>

Potom otvorte vlastnosti projektu na strane "Generátor kódu".

Kliknutím na odkaz otvorte používateľské rozhranie vlastností ladenia.

Odstráňte pôvodný profil, pretože ho nechcete používať.

Pridajte nový profil.

Vyberte položku Roslyn Component (Súčasť Roslyn).

Ak ste doteraz vykonali nastavenia, mali by ste byť schopní vybrať projekt aplikácie, takže ho vyberte.

Toto je koniec prípravy.

Skontrolujte, či môžete ladiť

Otvorte kód zdrojového generátora a Initialize na koniec metódy umiestnite bod prerušenia.

Poďme ladiť zdrojový generátor.

Ak sa proces zastaví v bode prerušenia, môžete potvrdiť, že ladíte normálne. Vďaka tomu by mal byť vývoj vášho zdrojového generátora primerane jednoduchý.

Zatiaľ vygenerujme pevný kód

Po prvé, pokúsme sa ľahko vydať pevný kód. Je to jednoduché, pretože kód nemusíte ani analyzovať. Aj keď ide o pevný kód, spracováva sa ako reťazec, takže je možné zvýšiť produkciu kódu v pevnej forme vytvorením programu.

Ak chcete výstup pevného kódu, context.RegisterPostInitializationOutput môžete tak urobiť pomocou . Nasleduje príklad výstupu kódu.

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

Obsah kódu je taký, ako je napísané v komentároch, takže podrobnosti vynechám. Vytvorte ho a uistite sa, že nie sú žiadne chyby.

Po dokončení zostavenia môžete v aplikačnom projekte rozbaliť "Analyzátory" a zobraziť kód vygenerovaný generátorom kódu.

Ak sa nezobrazuje, reštartujte Visual Studio a skontrolujte ho. Zdá sa, že v súčasnosti sa Visual Studio nemusí aktualizovať, pokiaľ ho nereštartujete. Inteligencia a zvýraznenie syntaxe, ktoré sa používajú pri vytváraní programov, sú podobné. Keďže sa však samotný kód odráža v čase zostavenia, zdá sa, že program sa odráža na strane aplikácie.

Po vygenerovaní kódu ho skúste použiť vo svojej aplikácii. Malo by to fungovať dobre.

Analýza a generovanie kódu

Ak kód iba normálne spracujete a vydáte ho, príliš sa nelíši od iného automatického generovania kódu a nemôžete využívať výhody zdrojového generátora. Teraz teda analyzujme kód aplikačného projektu a podľa toho vygenerujme kód.

Analýza kódu je však dosť hlboká, takže tu nemôžem vysvetliť všetko. Zatiaľ vysvetlím až do bodu, kedy môžete kód analyzovať a vydať. Ak chcete ísť hlbšie, urobte si vlastný prieskum.

Zatiaľ by som ako príklad chcel automaticky vytvoriť kód "pridať metódu do Reset všetkých vytvorených tried a resetovať hodnotu default všetkých vlastností na ". V posledných rokoch sa zdá, že sa uprednostňuje spracovanie nemenných inštancií, ale v herných programoch nie je možné vytvoriť novú inštanciu v každom rámci, takže si myslím, že proces resetovania hodnoty má využitie. default Možno budete chcieť nastaviť niečo iné, ale ak to urobíte, bude to dlhý prvý príklad, takže ho prosím použite sami.

Strana generátora kódu

Vytvorte novú triedu za účelom zachovania generátora, ktorý ste vytvorili predtým. Ak sa zmení obsah, ktorý sa má automaticky generovať, je lepšie vytvoriť nový v inej triede.

Kompilátor Roslyn sa postará o štruktúrovanie kódu, takže to, čo tu urobíme, je analyzovať štruktúrované údaje a napísať kód.

Najprv zverejním všetok kód. Snažil som sa mať čo najmenej kódu. Tentokrát to funguje, ale je to len na úrovni, ktorá sa pohybuje ako pán/pani, takže keď skutočne pokračujete vo vývoji, bude tam chýbať dosť častí. Prosím, zlepšite to tam podľa potreby.

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

Písal som o tom v komentároch, ale na niektorých miestach to vysvetlím.

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

context.SyntaxProvider.CreateSyntaxProvider analyzovať kód v aplikačnom projekte a čo najviac ho štruktúrovať. Všetko je rozdelené do uzlov a my pomocou nich určujeme, ktorý z nich predicate chceme spracovať. V prípade potreby transform tiež preveďte spracovaný objekt pomocou a odovzdajte ho na výstupnú stranu.

predicate V tomto prípade robíme nasledovné.

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

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

Po prvé, ako pri každom procese, proces automatického generovania kódu je možné vždy predčasne zrušiť. Preto použite token zrušenia na ThrowIfCancellationRequested zavolanie, aby ste ho mohli kedykoľvek prerušiť.

predicate Teraz, keď sa volá každý uzol, chceme určiť, ktorý z nich je ten, ktorý chceme spracovať. Keďže ich je obrovské množstvo, je lepšie ich tu do určitej miery zúžiť.

Keďže tentokrát pridáme do triedy spracovanie, určíme, či je a určíme ClassDeclarationSyntax , či bude spracovaná iba trieda. partial class Tiež, pretože kód je pripojený s , je partial class uvedený ako rozsudok.

// 対象のノードをコード出力に必要な形に変換します
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 Umožňuje previesť analýzu do požadovaného formulára a odovzdať ju výstupu kódu. Tentokrát nerobíme veľa konverzie, ale odovzdávame hodnotu na výstupnú stranu tak, ako je return . Cestou dostávame "deklarované symboly", aleSemanticModel môžeme získať veľa ďalších informácií pomocou a Syntac , takže si myslím, že ich môžeme získať, ak to bude potrebné. return N-tice sa vytvárajú a vracajú, ale konfigurácia údajov, ktoré sa majú odovzdať na výstupnú stranu, môže byť akákoľvek. Ak chcete odovzdať viac údajov, môžete vytvoriť takúto n-ticu, alebo môžete definovať svoju vlastnú triedu a odovzdať ju.

// 解析し変換した情報をもとにコードを出力します
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 Teraz vygenerujeme a vygenerujeme kód na základe údajov, ktoré má spracovať . context.SyntaxProvider.CreateSyntaxProvider return Hodnota of môže byť prijatá ako druhý argument , Action takže kód sa generuje na základe tejto hodnoty.

V tomto prípade vytvárame kód na získanie zoznamu syntaxí vlastností zo syntaxe triedy a nastavenie default vlastnosti z každého názvu.

Potom vytvorte metódu založenú na Reset názve triedy, vložte kód zoznamu vlastností vytvoreného skôr a nastavte hodnotu default všetkých vlastností späť Reset na Metóda je dokončená.

Kód je contextSource.AddSource výstupom samostatne pre každú triedu v metóde. Mimochodom, dôvodom, prečo som do názvu súboru vložil "g", je uľahčiť určenie, či ide o manuálne vytvorený kód alebo automaticky generovanú chybu kódu, keď sa vyskytne chyba zostavenia.

Ak to skutočne urobíte, dostanete požiadavky ako "Chcem resetovať pole", "Chcem ho inicializovať inak ako predvolene", "Chcem resetovať iba automatické vlastnosti". Ak ich vložíte, šnúra bude dlhá, takže sa ju snažte vyrobiť sami.

Aplikačný projekt

Tentokrát som vytvoril kód, ktorý rozširuje metódu triedy, takže triedu vhodne vytvorím.

Obsah je nasledujúci, ale ak existujú vlastnosti, zvyšok môžete použiť podľa potreby.

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

Metóda je rozšírená pri Reset vytváraní kódu, ale nemusí sa prejaviť v programe Visual Studio, preto v tom čase reštartujte Visual Studio. Myslím, že vidíte, že kód sa automaticky rozširuje na analyzátor.

Odsadenie je zvláštne, ale môžete ho ignorovať, pretože sa v podstate nedotknete automaticky vygenerovaného kódu.

Program.cs Skúste napísať kód, aby ste zistiliReset, či môžete zavolať metódu. Myslím si, že výsledky realizácie sa odrážajú podľa očakávania.

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