Използвайте Visual Studio и Source Generator за автоматично генериране на код

Страницата се актуализира :
Дата на създаване на страница :

Работна среда

Визуално студио
  • Визуално студио 2022
.НЕТЕН
  • .НЕТ 8.0

Предпоставки

Визуално студио
  • Работи дори с малко по-стара версия
.НЕТЕН
  • Работи дори с малко по-стара версия

Отначало

Има няколко техники за автоматично генериране на код със собствени дефиниции, но в тази статия ще ви покажа как да използвате генератор на източници. Едно от най-големите предимства на генератора на източници е, че анализира структурата на изходния код на текущия проект и генерира нов код въз основа на него. Например, когато създавате нов клас, можете да го направите така, че кодът автоматично да се добавя, за да съответства на класа. Можете да програмирате какъв код искате да генерирате, така че да можете да създадете всяка форма на автоматично генериране на код, която ви харесва.

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

В тази статия ще обясня как да проверя дали кодът се генерира автоматично, така че няма да стигам дотам, че всъщност да анализирам кода дълбоко и да изпълнявам усъвършенстван изход. Моля, проверете го сами като приложение.

Настройка

Първо, инсталирайте Visual Studio. Кратко обяснение е обобщено в следните съвети.

По принцип можете да го използвате за всеки проект, така че няма значение кое натоварване сте настроили. Въпреки това, този път, като "индивидуален компонент", ". NET компилатор платформа SDK. Това е полезно за отстраняване на грешки по време на разработването на Source Generator. Ако вече имате инсталиран Visual Studio, можете да го добавите от менюто на Visual Studio под Инструменти > Получаване на инструменти и функции.

Създаване и подготовка на проект за генератор на източници

Source Generator се създава в проект, отделен от основния проект на приложението. Няма значение дали първо ще ги създадете, или ще създадете допълнителни по-късно. В този случай ще го създам от проекта Source Generator.

В екрана Създаване на нов проект изберете Библиотека с класове.

Името на проекта може да бъде всичко, но засега CodeGenerator ще го оставим като .

За библиотеки с класове генераторът на източници в момента поддържа . 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>

След това отворете свойствата на проекта от "Генератор на кодове".

Щракнете върху връзката, за да отворите потребителския интерфейс на свойствата за стартиране на отстраняване на грешки.

Изтрийте оригиналния профил, защото не искате да го използвате.

Добавяне на нов профил.

Изберете Рослин компонент.

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

Това е краят на подготовката.

Проверете дали можете да отстранявате грешки

Отворете изходния генераторен код и 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 Стойността на може да бъде получена като втори аргумент на , така че кодът се генерира въз основа на 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();