Použití sady Visual Studio a generátoru zdrojů k automatickému generování kódu

Stránky aktualizovány :
Datum vytvoření stránky :

Provozní prostředí

Visual Studio
  • Visual Studio 2022
.SÍŤ
  • .NET 8.0

Požadavky

Visual Studio
  • Funguje i s poněkud starší verzí
.SÍŤ
  • Funguje i s poněkud starší verzí

Nejprve

Existuje několik technik pro automatické generování kódu s vlastními definicemi, ale v tomto článku vám ukážu, jak používat generátor zdrojů. Jednou z největších výhod generátoru zdrojů je, že analyzuje strukturu zdrojového kódu aktuálního projektu a na jeho základě generuje nový kód. Například při vytváření nové třídy můžete nastavit, aby byl kód automaticky přidán tak, aby odpovídal třídě. Můžete naprogramovat, jaký druh kódu chcete vygenerovat, takže můžete vytvořit libovolnou formu automatického generování kódu, která se vám líbí.

Kód je v podstatě automaticky generován na pozadí a začleněn do projektu v zákulisí. Vzhledem k tomu, že není výstupem jako soubor viditelným způsobem, nepoužívá se pro účely opětovného použití automaticky generovaného kódu pro obecné účely (i když jej lze prozatím odstranit jeho zkopírováním). Vzhledem k tomu, že kód je generován automaticky podle struktury projektu, snižuje náklady na ruční zadávání a odpovídajícím způsobem snižuje chyby při psaní kódu, což je obrovská výhoda.

V tomto článku vysvětlím, jak zkontrolovat, zda se kód generuje automaticky, takže nepůjdu tak daleko, abych kód skutečně do hloubky analyzoval a provedl pokročilý výstup. Vyhledejte si to prosím sami jako aplikaci.

sestava

Nejprve nainstalujte Visual Studio. Stručné vysvětlení je shrnuto v následujících tipech.

V podstatě jej můžete použít pro jakýkoli projekt, takže nezáleží na tom, jakou pracovní zátěž nastavíte. Tentokrát však jako "samostatná komponenta", ". NET Compiler Platform SDK. To je užitečné pro ladění během vývoje generátoru zdrojů. Pokud už máte nainstalovanou sadu Visual Studio, můžete ji přidat z nabídky sady Visual Studio v části Nástroje > Získat nástroje a funkce.

Vytvoření a příprava projektu generátoru zdrojů

Generátor zdrojů je vytvořen v projektu odděleném od hlavního projektu aplikace. Nezáleží na tom, zda je nejprve vytvoříte nebo později vytvoříte další. V tomto případě jej vytvořím z projektu Generátor zdrojů.

Na obrazovce Vytvořit nový projekt vyberte Knihovna tříd.

Název projektu může být jakýkoliv, ale prozatím CodeGenerator jej ponecháme jako .

U knihoven tříd generátor zdrojů v současné době podporuje . NET Standard 2.0.

Po vytvoření projektu získejte balíček s Microsoft.CodeAnalysis.CSharp NuGetem. Chování se může lišit v závislosti na verzi, ale nemá smysl nadále používat starou verzi, takže dám nejnovější verzi.

Pak otevřete soubor projektu jako kód.

Když jej otevřete, uvidíte následující.

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

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

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

</Project>

Přidejte jej následujícím způsobem: Neváhejte přidat cokoli dalšího, co potřebujete.

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

Dále napište kód pro třídu, která automaticky vygeneruje kód. V první řadě vytvoříme pouze framework, proto prosím přepište stávající kód od začátku Class1.cs nebo přidejte nový kód.

Kód by měl vypadat takto: Název třídy může být libovolný, ale je lepší mít název, který ukazuje, jaký druh kódu se automaticky vygeneruje. SampleGenerator Prozatím to nechte tak, jak . Initialize V této metodě napíšete analýzu kódu a proces generování kódu.

using Microsoft.CodeAnalysis;

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

Dále vytvořte projekt pro aplikaci, pro kterou chcete automaticky generovat kód. Může to být cokoli, ale zde jej jednoduše použijeme jako konzolovou aplikaci. Typ a verze rámce projektu, pro který je kód automaticky generován, odpovídá obecnému číslu.

Přidejte odkaz na projekt generátoru zdrojového kódu z projektu na straně aplikace.

Některá nastavení nelze nastavit ve vlastnostech, proto otevřete soubor projektu v kódu.

Myslím, že to vypadá 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>

Přidejte nastavení pro odkazovaný projekt následujícím způsobem: Ujistěte se, že jste neudělali 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>

Dále otevřete vlastnosti projektu na straně "Generátor kódů".

Kliknutím na odkaz otevřete uživatelské rozhraní Vlastnosti spuštění ladění.

Odstraňte původní profil, protože ho nechcete používat.

Přidejte nový profil.

Vyberte možnost Součást Roslyn.

Pokud jste dosud provedli nastavení, měli byste být schopni vybrat projekt aplikace, takže jej vyberte.

Tím příprava končí.

Zkontrolujte, jestli můžete ladit

Otevřete zdrojový kód generátoru a Initialize umístěte zarážku na konec metody.

Pojďme ladit generátor zdrojů.

Pokud se proces zastaví na zarážce, můžete potvrdit, že ladíte normálně. To by mělo usnadnit vývoj vašeho generátoru zdrojů.

Prozatím vypíšeme pevný kód

Nejprve se pokusíme snadno vygenerovat pevný kód. Je to snadné, protože nemusíte ani analyzovat kód. I když se jedná o pevný kód, je s ním zacházeno jako s řetězcem, takže je možné zvýšit produkci kódu v pevné podobě sestavením programu.

Pokud chcete vytvořit pevný kód, context.RegisterPostInitializationOutput můžete tak učinit pomocí . Následuje pří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 takový, jak je napsáno v komentářích, takže podrobnosti vynechám. Sestavte jej a ujistěte se, že v něm nejsou žádné chyby.

Po dokončení sestavení můžete rozbalit "Analyzátory" v projektu aplikace a zobrazit kód vygenerovaný generátorem kódu.

Pokud ho nevidíte, restartujte Visual Studio a zkontrolujte ho. Zdá se, že v tuto chvíli nemusí být Visual Studio aktualizováno, pokud ho nerestartujete. Intellisence a zvýraznění syntaxe používané při vytváření programů jsou podobné. Vzhledem k tomu, že samotný kód se projeví v době sestavení, zdá se, že program se projeví na straně aplikace.

Po vygenerování kódu ho zkuste použít ve své aplikaci. Mělo by to fungovat dobře.

Analýza a generování kódu

Pokud kód pouze normálně zpracujete a vypíšete, příliš se neliší od jiného automatického generování kódu a nemůžete využít výhod generátoru zdrojů. Nyní tedy analyzujme kód projektu aplikace a podle toho vygenerujme kód.

Analýza kódu je však poměrně hluboká, takže zde nemohu vysvětlit vše. Prozatím vysvětlím až do bodu, kdy můžete kód analyzovat a vydat. Pokud chcete jít hlouběji, udělejte si prosím vlastní průzkum.

Prozatím bych například rád automaticky vytvořil kód "přidat metodu do Reset všech vytvořených tříd a resetovat hodnotu default všech vlastností na ". V posledních letech se zdá, že je upřednostňována práce s neměnnými instancemi, ale v herních programech není možné vytvořit novou instanci každý snímek, takže si myslím, že proces resetování hodnoty má využití. default Možná budete chtít nastavit něco jiného než toto, ale pokud to uděláte, bude to dlouhý první příklad, takže to prosím použijte sami.

Strana generátoru kódu

Vytvořte novou třídu za účelem zachování generátoru, který jste vytvořili dříve. Pokud se změní obsah, který má být automaticky generován, je lepší vytvořit nový v jiné třídě.

Kompilátor Roslyn se stará o strukturování kódu, takže to, co zde uděláme, je analyzovat strukturovaná data a napsat kód.

Nejprve zveřejním veškerý kód. Snažil jsem se mít co nejméně kódu. Tentokrát to funguje, ale je to pouze na úrovni, která se pohybuje jako pan/paní, takže když skutečně pokračujete ve vývoji, bude chybět docela dost částí. Prosím, vylepšete to podle potřeby.

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

Psal jsem o tom v komentářích, ale na některých místech to vysvětlím.

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

context.SyntaxProvider.CreateSyntaxProvider analyzovat kód v projektu aplikace a co nejvíce jej strukturovat. Vše je rozděleno na uzly a my pomocí nich určujeme predicate , které z nich chceme zpracovat. V případě potřeby transform také převeďte zpracovaný objekt pomocí a předejte jej na výstupní stranu.

predicate V tomto případě postupujeme následovně.

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

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

Za prvé, stejně jako u každého procesu, proces automatického generování kódu lze vždy předčasně zrušit. K volání tedy použijte token ThrowIfCancellationRequested zrušení, abyste mohli kdykoli přerušit.

predicate Teď, když je volán každý uzel, chceme určit, který z nich je ten, který chceme zpracovat. Vzhledem k tomu, že je jich obrovské množství, je lepší je zde do určité míry zúžit.

Vzhledem k tomu, že tentokrát do třídy přidáme zpracování, určíme, zda je, a určíme ClassDeclarationSyntax , zda bude zpracována pouze třída. partial class Vzhledem k tomu, že kód je připojen k , partial class je také uveden jako rozsudek.

// 対象のノードをコード出力に必要な形に変換します
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 převést analýzu do požadované podoby a předat ji výstupu kódu. Tentokrát neděláme moc konverze, ale předáváme hodnotu na výstupní stranu tak, jak je return . Postupně získáváme "deklarované symboly", aleSemanticModel pomocí a Syntac , takže si myslím, že je můžeme získat, pokud to bude nutné. return Řazené kolekce členů jsou vytvářeny a vráceny, ale konfigurace dat, která mají být předána na výstupní stranu, může být libovolná. Pokud chcete předat více dat, můžete vytvořit n-tici, jako je tato, nebo můžete definovat vlastní třídu a předat ji.

// 解析し変換した情報をもとにコードを出力します
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 Nyní vygenerujeme a vypíšeme kód na základě dat, která má zpracovat . context.SyntaxProvider.CreateSyntaxProvider return Hodnota of může být přijata jako druhý argument , Action takže kód je generován na základě této hodnoty.

V tomto případě vytváříme kód pro získání seznamu syntaxe vlastností ze syntaxe třídy a nastavení default vlastnosti na z každého názvu.

Poté vytvořte metodu založenou na Reset názvu třídy, vložte kód seznamu vlastností vytvořeného dříve a nastavte hodnotu default všech vlastností zpět Reset na Metoda je dokončena.

Kód je contextSource.AddSource výstupem samostatně pro každou třídu v metodě. Mimochodem, důvodem, proč jsem do názvu souboru vložil "g", je usnadnit určení, zda se jedná o ručně vytvořený kód nebo automaticky generovanou chybu kódu, když dojde k chybě sestavení.

Pokud to skutečně uděláte, budete dostávat požadavky jako "Chci pole resetovat", "Chci jej inicializovat jinak než výchozí", "Chci resetovat pouze automatické vlastnosti". Pokud je vložíte, šňůra bude dlouhá, takže se ji snažte vyrobit sami.

Strana aplikačního projektu

Tentokrát jsem vytvořil kód, který rozšiřuje metodu třídy, takže třídu vytvořím vhodně.

Obsah je následující, ale pokud existují vlastnosti, můžete podle potřeby použít zbytek.

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

Metoda je rozšířena při Reset vytváření kódu, ale nemusí se projevit v aplikaci Visual Studio, proto restartujte Visual Studio v té době. Myslím, že můžete vidět, že kód je automaticky rozšířen na analyzátor.

Odsazení je zvláštní, ale můžete ho ignorovat, protože se v podstatě nedotýkáte automaticky generovaného kódu.

Program.cs Zkuste zapsat kód, abyste zjistiliReset, jestli můžete metodu volat. Myslím, že výsledky exekuce se projevily podle očekávání.

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