Використовуйте Visual Studio та Source Generator для автоматичної генерації коду

Сторінка оновлюється :
Дата створення сторінки :

Робоче середовище

Візуальна студія
  • Visual Studio 2022
.МЕРЕЖІ
  • .NET 8.0

Передумови

Візуальна студія
  • Працює навіть з дещо старішою версією
.МЕРЕЖІ
  • Працює навіть з дещо старішою версією

Спочатку

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

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

У цій статті я поясню, як перевірити, що код генерується автоматично, тому я не буду заходити так далеко, щоб насправді глибоко аналізувати код і виконувати розширений висновок. Будь ласка, подивіться його самі як додаток.

Установки

Спочатку встановіть Visual Studio. Коротке пояснення узагальнено в наступних порадах.

По суті, ви можете використовувати його для будь-якого проекту, тому не має значення, яке робоче навантаження ви налаштували. Щоправда, цього разу, як «індивідуальна складова», . NET Compiler Platform SDK. Це корисно для налагодження під час розробки Source Generator. Якщо у вас уже встановлено програму Visual Studio, її можна додати з меню Visual Studio в розділі «Інструменти» > «Отримати інструменти та засоби».

Створення та підготовка проекту генератора джерел

Source Generator створюється в проекті, окремому від основного проекту програми. Неважливо, створюєте ви їх спочатку або створюєте додаткові пізніше. В даному випадку я буду створювати його з проекту Source Generator.

На екрані «Створити новий проект» виберіть «Бібліотека класів».

Назва проекту може бути будь-якою, але поки CodeGenerator що ми залишимо її як .

Для бібліотек класів Source Generator наразі підтримує . Стандарт NET 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 UI.

Видаліть вихідний профіль, оскільки не хочете його використовувати.

Додайте новий профіль.

Виберіть компонент Roslyn.

Якщо ви вже внесли налаштування, ви зможете вибрати проект програми, тому виберіть його.

На цьому підготовка закінчена.

Перевірте, чи вмієте ви налагоджувати

Відкрийте код вихідного генератора та 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 може не оновлюватися, якщо ви не перезапустите його. Підсвічування Intellisence і синтаксису, що використовуються при створенні програм, схожі. Однак, оскільки сам код відображається в момент збірки, складається враження, що програма відбивається на стороні програми.

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

Аналізуйте та генеруйте код

Якщо ви просто нормально обробляєте код і виводите його, він мало чим відрізняється від іншої автоматичної генерації коду, і ви не можете скористатися перевагами генератора вихідних кодів. Отже, тепер давайте проаналізуємо код проекту додатку та згенеруємо код відповідним чином.

Однак аналіз коду досить глибокий, тому я не можу тут все пояснити. Поки що я поясню до того моменту, коли ви зможете проаналізувати та вивести код. Якщо ви хочете заглибитися, будь ласка, проведіть власне дослідження.

Наразі, як приклад, хотілося б автоматично створити код "додати метод до 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();