Utiliser Visual Studio et Source Generator pour générer automatiquement du code

Page mise à jour :
Date de création de la page :

Environnement d’exploitation

Studio visuel
  • Visual Studio 2022
.FILET
  • .NET 8.0

Conditions préalables

Studio visuel
  • Cela fonctionne même avec une version un peu plus ancienne
.FILET
  • Cela fonctionne même avec une version un peu plus ancienne

Au début

Il existe plusieurs techniques pour générer automatiquement du code avec vos propres définitions, mais dans cet article, je vais vous montrer comment utiliser un générateur de source. L’un des plus grands avantages d’un générateur de source est qu’il analyse la structure du code source du projet en cours et génère un nouveau code basé sur celui-ci. Par exemple, lorsque vous créez une classe, vous pouvez faire en sorte que le code soit automatiquement ajouté pour correspondre à la classe. Vous pouvez programmer le type de code que vous souhaitez générer, afin de créer n’importe quelle forme de génération automatique de code que vous aimez.

Le code est essentiellement généré automatiquement en arrière-plan et incorporé dans le projet en coulisses. Comme il n’est pas affiché sous forme de fichier de manière visible, il n’est pas utilisé dans le but de réutiliser le code généré automatiquement à des fins générales (bien qu’il puisse être supprimé en le copiant pour le moment). Cependant, comme le code est généré automatiquement en fonction de la structure du projet, cela réduit le coût de la saisie manuelle et réduit les erreurs d’écriture de code en conséquence, ce qui est un énorme avantage.

Dans cet article, je vais vous expliquer comment vérifier que le code est généré automatiquement, donc je n’irai pas jusqu’à analyser le code en profondeur et effectuer une sortie avancée. Veuillez le rechercher vous-même en tant qu’application.

coup monté

Tout d’abord, installez Visual Studio. Une brève explication est résumée dans les conseils suivants.

En gros, vous pouvez l’utiliser pour n’importe quel projet, donc peu importe la charge de travail que vous configurez. Cependant, cette fois, en tant que « composant individuel », « . Kit de développement logiciel (SDK) de la plate-forme du compilateur NET. Ceci est utile pour le débogage pendant le développement de Source Generator. Si Visual Studio est déjà installé, vous pouvez l’ajouter à partir du menu Visual Studio sous Outils > Obtenir des outils et des fonctionnalités.

Création et préparation d’un projet de générateur de source

Le générateur de source est créé dans un projet distinct du projet d’application principal. Peu importe que vous les créiez d’abord ou que vous en créiez d’autres plus tard. Dans ce cas, je vais le créer à partir du projet Source Generator.

Sur l’écran Créer un projet, sélectionnez Bibliothèque de classes.

Le nom du projet peut être n’importe quoi, mais pour l’instant CodeGenerator , nous le laisserons sous la forme .

Pour les bibliothèques de classes, Source Generator prend actuellement en charge . Norme NET 2.0.

Une fois que vous avez créé votre projet, obtenez le package avec Microsoft.CodeAnalysis.CSharp NuGet. Le comportement peut différer selon la version, mais il ne sert à rien de continuer à utiliser l’ancienne version, je vais donc mettre la dernière version.

Ouvrez ensuite le fichier projet en tant que code.

Lorsque vous l’ouvrez, vous verrez ce qui suit.

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

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

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

</Project>

Ajoutez-le comme suit : N’hésitez pas à ajouter tout ce dont vous avez besoin.

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

Ensuite, écrivez le code de la classe qui générera automatiquement le code. Tout d’abord, nous ne créerons qu’un framework, veuillez donc réécrire le code existant depuis le début Class1.cs ou ajouter un nouveau code.

Le code doit ressembler à ceci : Le nom de la classe peut être n’importe quoi, mais il est préférable d’avoir un nom qui montre quel type de code est généré automatiquement. SampleGenerator Pour l’instant, laissez-le comme . Initialize Dans cette méthode, vous allez écrire le processus d’analyse et de génération de code.

using Microsoft.CodeAnalysis;

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

Ensuite, créez un projet pour l’application pour laquelle vous souhaitez générer automatiquement du code. Cela peut être n’importe quoi, mais ici nous allons simplement l’utiliser comme une application console. Le type et la version du cadre du projet pour lequel le code est généré automatiquement correspondent au numéro général.

Ajoutez une référence au projet de générateur de source à partir du projet côté application.

Certains paramètres ne peuvent pas être définis dans les propriétés, ouvrez donc le fichier projet dans le code.

Je pense que cela ressemble à ceci :

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

Ajoutez les paramètres du projet référencé comme suit : Assurez-vous de ne pas faire d’erreur dans la syntaxe du 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>

Ensuite, ouvrez les propriétés du projet du côté « Code Generator ».

Cliquez sur le lien pour ouvrir l’interface utilisateur des propriétés de lancement de débogage.

Supprimez le profil d’origine car vous ne souhaitez pas l’utiliser.

Ajoutez un nouveau profil.

Sélectionnez Composant Roslyn.

Si vous avez effectué les réglages jusqu’à présent, vous devriez pouvoir sélectionner le projet d’application, alors sélectionnez-le.

C’est la fin de la préparation.

Vérifiez si vous pouvez déboguer

Ouvrez le code du générateur source et Initialize placez un point d’arrêt à la fin de la méthode.

Déboguons le générateur de source.

Si le processus s’arrête au point d’arrêt, vous pouvez confirmer que vous déboguez normalement. Cela devrait rendre le développement de votre générateur de source raisonnablement facile.

Pour l’instant, sortons un code fixe

Tout d’abord, essayons de sortir facilement un code fixe. C’est facile car vous n’avez même pas besoin d’analyser le code. Même s’il s’agit d’un code fixe, il est traité comme une chaîne de caractères, il est donc possible d’augmenter la production de code sous une forme fixe en construisant un programme.

Si vous souhaitez générer un code fixe, vous context.RegisterPostInitializationOutput pouvez le faire à l’aide de . Voici un exemple de sortie de code.

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

Le contenu du code est tel qu’il est écrit dans les commentaires, je vais donc omettre les détails. Construisez-le et assurez-vous qu’il n’y a pas d’erreurs.

Une fois la génération terminée, vous pouvez développer « Analyseurs » dans le projet d’application pour voir le code généré par le générateur de code.

Si vous ne le voyez pas, redémarrez Visual Studio et vérifiez-le. Il semble qu’à l’heure actuelle, Visual Studio ne soit pas mis à jour à moins que vous ne le redémarriez. L’Intellisence et la coloration syntaxique utilisées lors de la création de programmes sont similaires. Cependant, comme le code lui-même est reflété au moment de la construction, il semble que le programme soit reflété du côté de l’application.

Une fois le code généré, essayez de l’utiliser dans votre application. Cela devrait bien fonctionner.

Analyser et générer du code

Si vous traitez simplement le code normalement et que vous le produisez, il n’est pas très différent des autres générateurs automatiques de code, et vous ne pouvez pas profiter des avantages du générateur de source. Analysons maintenant le code du projet d’application et générons le code en conséquence.

Cependant, l’analyse du code est assez approfondie, je ne peux donc pas tout expliquer ici. Pour l’instant, je vais vous expliquer jusqu’au point où vous pouvez analyser et sortir le code. Si vous voulez aller plus loin, veuillez faire vos propres recherches.

Pour l’instant, à titre d’exemple, j’aimerais créer automatiquement le code « ajouter une méthode à Reset toutes les classes créées et réinitialiser la valeur default de toutes les propriétés à « . Ces dernières années, il semble que la gestion des instances immuables ait été préférée, mais dans les programmes de jeu, il n’est pas possible de créer une nouvelle instance à chaque image, donc je pense qu’il y a une utilité pour le processus de réinitialisation de la valeur. default Vous voudrez peut-être définir autre chose que cela, mais si vous le faites, ce sera un long premier exemple, alors appliquez-le vous-même.

Côté générateur de code

Créez une nouvelle classe dans le but de conserver le générateur que vous avez créé précédemment. Si le contenu à générer automatiquement change, il est préférable d’en créer un nouveau dans une autre classe.

Le compilateur Roslyn s’occupe de la structuration du code, donc ce que nous allons faire ici est d’analyser les données structurées et d’écrire le code.

Tout d’abord, je vais poster tout le code. J’ai essayé d’avoir le moins de code possible. Cette fois, cela fonctionne, mais ce n’est qu’à un niveau qui évolue en tant que M./Mme, donc lorsque vous procéderez réellement au développement, il y aura pas mal de pièces manquantes. Veuillez améliorer si nécessaire.

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

J’ai écrit à ce sujet dans les commentaires, mais je l’expliquerai à certains endroits.

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

context.SyntaxProvider.CreateSyntaxProvider pour analyser le code dans le projet d’application et le structurer autant que possible. Tout est divisé en nœuds, et nous les utilisons pour déterminer lequel d’entre eux nous predicate voulons traiter. Aussi, si nécessaire transform , convertissez l’objet traité avec et passez-le du côté sortie.

predicate Dans ce cas, nous procédons comme suit.

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

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

Tout d’abord, comme pour tout processus, le processus de génération automatique de code peut toujours être annulé prématurément. Utilisez donc le jeton d’annulation pour ThrowIfCancellationRequested appeler afin de pouvoir interrompre à tout moment.

predicate Maintenant que chaque nœud est appelé, nous voulons déterminer lequel est celui que nous voulons traiter. Comme il y en a un grand nombre, il est préférable de les réduire ici dans une certaine mesure.

Puisque nous allons ajouter le traitement à la classe cette fois, nous ClassDeclarationSyntax allons déterminer si c’est le cas et déterminer si seule la classe sera traitée. partial class De plus, puisque le code est attaché à , il partial class est mis comme un jugement.

// 対象のノードをコード出力に必要な形に変換します
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 vous permet de convertir l’analyse dans la forme requise et de la transmettre à la sortie du code. Cette fois, nous ne faisons pas beaucoup de conversion, mais passons la valeur du côté de la sortie telle quelle return . Nous obtenons des « symboles déclarés » en cours de route, maisSemanticModel nous pouvons obtenir beaucoup d’informations supplémentaires en utilisant et Syntac , donc je pense que nous pouvons l’obtenir si nécessaire. return Des tuples sont créés et renvoyés, mais la configuration des données à transmettre au côté de la sortie peut être n’importe quoi. Si vous souhaitez transmettre plusieurs données, vous pouvez créer un tuple comme celui-ci, ou vous pouvez définir votre propre classe et la transmettre.

// 解析し変換した情報をもとにコードを出力します
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 Maintenant, nous allons générer et afficher du code basé sur les données à traiter par . context.SyntaxProvider.CreateSyntaxProvider return La valeur de peut être reçue comme deuxième argument de , de sorte que le code est généré en fonction de Action cette valeur.

Dans ce cas, nous créons du code pour obtenir la liste syntaxique des propriétés à partir de la syntaxe de la classe et définir la default propriété sur à partir de chaque nom.

Après cela, créez une méthode basée sur Reset le nom de la classe, incorporez le code de la liste de propriétés créée précédemment et redéfinissez Reset la valeur default de toutes les propriétés sur La méthode est terminée.

Le code est contextSource.AddSource généré séparément pour chaque classe de la méthode. D’ailleurs, la raison pour laquelle j’ai mis « g » dans le nom du fichier est de faciliter la détermination s’il s’agit d’un code créé manuellement ou d’une erreur de code générée automatiquement lorsqu’il y a une erreur de construction.

Si vous le faites réellement, vous recevrez des requêtes telles que « Je veux réinitialiser le champ », « Je veux l’initialiser autrement que par défaut », « Je veux réinitialiser uniquement les propriétés automatiques ». Si vous les mettez, le cordon sera long, alors essayez de le faire vous-même.

Côté projet d’application

Cette fois, j’ai créé un code qui étend la méthode de la classe, je vais donc créer une classe de manière appropriée.

Le contenu est le suivant, mais s’il y a des propriétés, vous pouvez utiliser le reste selon les besoins.

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

La méthode est étendue lorsque Reset vous créez le code, mais elle peut ne pas être reflétée dans Visual Studio, alors redémarrez Visual Studio à ce moment-là. Je pense que vous pouvez voir que le code est automatiquement étendu à l’analyseur.

L’indentation est étrange, mais vous pouvez l’ignorer car vous ne touchez pas au code généré automatiquement.

Program.cs Essayez d’écrire du code pour voir Reset si vous pouvez appeler la méthode. Je pense que les résultats de l’exécution sont reflétés comme prévu.

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