Использование Visual Studio и генератора исходного кода для автоматического создания кода

Страница обновлена :
Дата создания страницы :

Условия эксплуатации

Визуальная студия
  • Visual Studio 2022
.СЕТЬ
  • .NET 8.0

Необходимые условия

Визуальная студия
  • Работает даже с несколько более старой версией
.СЕТЬ
  • Работает даже с несколько более старой версией

Сначала

Существует несколько методов автоматической генерации кода с собственными определениями, но в этой статье я покажу вам, как использовать генератор исходного кода. Одним из самых больших преимуществ генератора исходного кода является то, что он анализирует структуру исходного кода текущего проекта и генерирует новый код на ее основе. Например, при создании нового класса можно сделать так, чтобы код автоматически добавлялся в соответствии с классом. Вы можете запрограммировать, какой тип кода вы хотите сгенерировать, поэтому вы можете создать любую форму автоматической генерации кода, которая вам нравится.

Код, по сути, автоматически генерируется в фоновом режиме и включается в проект в фоновом режиме. Поскольку он не выводится в виде файла в видимом виде, он не используется с целью повторного использования автоматически сгенерированного кода для общих целей (хотя его можно удалить, скопировав его на данный момент). Однако, поскольку код генерируется автоматически в соответствии со структурой проекта, это снижает затраты на ручной ввод и, соответственно, уменьшает количество ошибок при написании кода, что является огромным преимуществом.

В этой статье я объясню, как проверить, что код генерируется автоматически, поэтому я не буду заходить так далеко, чтобы на самом деле глубоко анализировать код и выполнять расширенный вывод. Пожалуйста, найдите его самостоятельно в качестве приложения.

настройка

Во-первых, установите Visual Studio. Краткое объяснение кратко изложено в следующих советах.

В принципе, вы можете использовать его для любого проекта, поэтому не имеет значения, какую рабочую нагрузку вы настроили. Однако на этот раз в качестве «индивидуальной составляющей», «. NET Compiler Platform SDK. Это полезно для отладки во время разработки генератора исходного кода. Если у вас уже установлена Visual Studio, вы можете добавить ее в меню Visual Studio в разделе Сервис > Получить средства и компоненты.

Создание и подготовка проекта генератора исходного кода

Генератор исходного кода создается в проекте, отдельном от основного проекта приложения. Не имеет значения, создадите ли вы их сначала или создадите дополнительные позже. В данном случае я создам его из проекта Source Generator.

На экране Create New Project (Создать новый проект) выберите Class Library (Библиотека классов).

Имя проекта может быть любым, но пока CodeGenerator мы оставим его как .

Для библиотек классов генератор исходного кода в настоящее время поддерживает . NET Standard 2.0.

После создания проекта получите пакет с Microsoft.CodeAnalysis.CSharp помощью NuGet. Поведение может отличаться в зависимости от версии, но продолжать использовать старую версию нет смысла, поэтому поставлю последнюю версию.

Затем откройте файл проекта в виде кода.

Когда вы откроете его, то увидите следующее.

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

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

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

</Project>

Добавьте его следующим образом: Не стесняйтесь добавлять все, что вам нужно.

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

Далее напишите код для класса, который будет автоматически генерировать код. Прежде всего, мы создадим только фреймворк, поэтому, пожалуйста, перепишите существующий код с самого начала Class1.cs или добавьте новый код.

Код должен выглядеть следующим образом: Имя класса может быть любым, но лучше иметь имя, которое показывает, какой код генерируется автоматически. SampleGenerator А пока оставьте его как . Initialize В этом методе вы напишете процесс анализа и генерации кода.

using Microsoft.CodeAnalysis;

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

Далее создайте проект для приложения, для которого вы хотите автоматически генерировать код. Это может быть что угодно, но здесь мы просто будем использовать его как консольное приложение. Тип и версия фреймворка проекта, к которому автоматически генерируется код, соответствует общему номеру.

Добавьте ссылку на проект генератора исходного кода из проекта на стороне приложения.

Некоторые настройки не могут быть заданы в свойствах, поэтому откройте файл проекта в коде.

По-моему, это выглядит так:

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

Добавьте настройки для проекта, на который указывает ссылка, следующим образом: Убедитесь, что вы не допустили ошибку в синтаксисе 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>

Далее откройте свойства проекта на стороне «Генератор кода».

Щелкните ссылку, чтобы открыть пользовательский интерфейс Debug Launch Properties.

Удалите исходный профиль, так как вы не хотите его использовать.

Добавьте новый профиль.

Выберите Roslyn Component.

Если вы уже выполнили настройки, вы сможете выбрать проект приложения, поэтому выберите его.

На этом подготовка заканчивается.

Проверьте, можете ли вы выполнять отладку

Откройте исходный код генератора и Initialize поместите точку останова в конце метода.

Отладим генератор исходного кода.

Если процесс останавливается в точке останова, можно подтвердить, что отладка выполняется нормально. Это должно сделать разработку генератора исходного кода достаточно простой.

А пока выведем фиксированный код

Во-первых, давайте попробуем легко вывести фиксированный код. Это просто, потому что вам даже не нужно анализировать код. Даже если это фиксированный код, он обрабатывается как строка, поэтому можно увеличить производство кода в фиксированном виде, построив программу.

Если вы хотите вывести фиксированный код, вы context.RegisterPostInitializationOutput можете сделать это с помощью . Ниже приведен пример выходных данных кода.

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

Содержимое кода так, как написано в комментариях, поэтому я опущу подробности. Соберите его и убедитесь в отсутствии ошибок.

Когда сборка будет завершена, вы можете развернуть "Анализаторы" в проекте приложения, чтобы увидеть код, сгенерированный генератором кода.

Если вы не видите его, перезапустите Visual Studio и проверьте его. Похоже, что в настоящее время Visual Studio может не обновляться, пока вы не перезапустите ее. Интеллектуализация и подсветка синтаксиса, используемые при создании программ, аналогичны. Однако, поскольку сам код отражается в момент сборки, создается впечатление, что программа отражается на стороне приложения.

После того, как код сгенерирован, попробуйте использовать его в своем приложении. Все должно работать нормально.

Анализ и генерация кода

Если вы просто обработаете код в обычном режиме и выведете его, он мало чем отличается от другой автоматической генерации кода, и вы не сможете воспользоваться преимуществами генератора исходного кода. Итак, теперь давайте проанализируем код проекта приложения и сгенерируем соответствующий код.

Тем не менее, анализ кода достаточно глубокий, поэтому я не могу объяснить здесь все. А пока я объясню до того момента, когда вы сможете проанализировать и вывести код. Если вы хотите углубиться в эту тему, пожалуйста, проведите собственное исследование.

Пока в качестве примера хотелось бы автоматически создать код "добавить метод ко Reset всем созданным классам и сбросить значение default всех свойств в ". В последние годы кажется, что обработка неизменяемых экземпляров была предпочтительной, но в игровых программах нет возможности создавать новый экземпляр каждый кадр, поэтому я думаю, что есть смысл в процессе сброса значения. default Возможно, вы захотите установить что-то другое, но если вы это сделаете, это будет длинный первый пример, поэтому, пожалуйста, примените его самостоятельно.

Сторона генератора кода

Создайте новый класс, чтобы сохранить генератор, который вы создали ранее. Если контент, который будет автоматически сгенерирован, изменяется, лучше создать новый в другом классе.

Компилятор Roslyn заботится о структурировании кода, поэтому здесь мы собираемся разобрать структурированные данные и написать код.

Во-первых, я выложу весь код. Я старался использовать как можно меньше кода. На этот раз это работает, но только на уровне, который движется как Мистер/Мисс, поэтому, когда вы действительно продолжите разработку, будет довольно много недостающих частей. Пожалуйста, улучшайте там по мере необходимости.

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

Я писал об этом в комментариях, но кое-где объясню.

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

context.SyntaxProvider.CreateSyntaxProvider проанализировать код в проекте приложения и максимально его структурировать. Все разбивается на узлы, и мы используем для определения, какой из них мы predicate хотим обработать. Также, при необходимости transform , преобразуем обрабатываемый объект с и передаем его на выходную сторону.

predicate В данном случае мы делаем следующее.

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

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

Во-первых, как и любой процесс, процесс автоматической генерации кода всегда можно отменить преждевременно. Поэтому используйте маркер отмены для ThrowIfCancellationRequested вызова, чтобы прервать его в любое время.

predicate Теперь, когда каждый узел вызван, мы хотим определить, какой из них мы хотим обработать. Так как их огромное количество, лучше в какой-то степени сузить их здесь.

Так как в этот раз мы собираемся добавить обработку в класс, мы ClassDeclarationSyntax определим, так ли это, и определим, будет ли обрабатываться только класс. partial class Кроме того, поскольку код прикреплен с , он partial class ставится в качестве суждения.

// 対象のノードをコード出力に必要な形に変換します
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 Позволяет преобразовать анализ в требуемый вид и передать его в вывод кода. На этот раз мы не делаем много преобразований, а передаем значение на выходную сторону в том виде, в котором оно есть return . Мы получаем «заявленные символы» по ходу дела, ноSemanticModel мы можем получить много дополнительной информации с помощью и Syntac , поэтому я думаю, что мы можем получить ее в случае необходимости. return Кортежи создаются и возвращаются, но конфигурация данных, передаваемых на сторону вывода, может быть любой. Если вы хотите передать несколько данных, вы можете создать кортеж, подобный этому, или вы можете определить свой собственный класс и передать его.

// 解析し変換した情報をもとにコードを出力します
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 Теперь мы сгенерируем и выведем код на основе данных, которые будут обрабатываться . context.SyntaxProvider.CreateSyntaxProvider return Значение of может быть получено в качестве второго аргумента Action , поэтому код генерируется на основе этого значения.

В данном случае мы создаем код для получения синтаксического списка свойств из синтаксиса класса и устанавливаем свойство default в значение из каждого имени.

После этого создадим метод на Reset основе имени класса, встроим код созданного ранее списка свойств и установим значение default всех свойств обратно Reset в Метод завершен.

Код выводится contextSource.AddSource отдельно для каждого класса в методе. Кстати, причина, по которой я ставлю "g" в имя файла, заключается в том, чтобы было легче определить, является ли это кодом, созданным вручную, или автоматически сгенерированной ошибкой кода, когда есть ошибка сборки.

Если вы действительно сделаете это, вы получите такие запросы, как «Я хочу сбросить поле», «Я хочу инициализировать его, отличное от значения по умолчанию», «Я хочу сбросить только автоматические свойства». Если вы их вставите, шнур будет длинным, поэтому попробуйте сделать его самостоятельно.

Прикладная сторона проекта

На этот раз я создал код, расширяющий метод класса, поэтому я создам класс соответствующим образом.

Содержимое выглядит следующим образом, но если есть свойства, вы можете использовать остальное по мере необходимости.

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

Метод расширяется при Reset создании кода, но он может не отражаться в Visual Studio, поэтому перезапустите Visual Studio в это время. Думаю, вы видите, что код автоматически расширяется на анализатор.

Отступ странный, но его можно игнорировать, потому что вы в принципе не трогаете автоматически сгенерированный код.

Program.cs Попробуйте написать код, чтобы проверитьReset, сможете ли вы вызвать метод. Я думаю, что результаты исполнения отражены так, как и ожидалось.

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