Използвайте 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.RegisterSourceOutput
context.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();