Koodi automaatseks genereerimiseks kasutage Visual Studio ja Source Generatorit
Töökeskkond
- Visuaalne stuudio
-
- Visual Studio 2022
- .NET
-
- .NET 8.0
Eeltingimused
- Visuaalne stuudio
-
- See töötab isegi mõnevõrra vanema versiooniga
- .NET
-
- See töötab isegi mõnevõrra vanema versiooniga
Alguses
Oma definitsioonidega koodi automaatseks genereerimiseks on mitmeid tehnikaid, kuid selles artiklis näitan teile, kuidas kasutada allikageneraatorit. Lähtegeneraatori üks suurimaid eeliseid on see, et see analüüsib praeguse projekti lähtekoodi struktuuri ja genereerib selle põhjal uue koodi. Näiteks kui loote uue klassi, saate selle muuta nii, et kood lisatakse automaatselt klassile vastavaks. Saate programmeerida, millist koodi soovite genereerida, nii et saate luua mis tahes vormis automaatse koodi genereerimise, mis teile meeldib.
Kood genereeritakse sisuliselt taustal automaatselt ja integreeritakse kulisside taga olevasse projekti. Kuna seda ei väljastata failina nähtaval viisil, ei kasutata seda automaatselt genereeritud koodi taaskasutamiseks üldistel eesmärkidel (kuigi seda saab praegu kopeerides eemaldada). Kuna aga kood genereeritakse automaatselt vastavalt projekti struktuurile, vähendab see käsitsi sisestamise kulusid ja vähendab vastavalt koodi kirjutamise vigu, mis on tohutu eelis.
Selles artiklis selgitan, kuidas kontrollida, kas kood genereeritakse automaatselt, nii et ma ei lähe nii kaugele, et koodi tegelikult põhjalikult analüüsida ja täiustatud väljundit teha. Palun otsige see ise rakendusena üles.
seadistus
Esmalt installige Visual Studio. Lühike selgitus on kokku võetud järgmistes nõuandes.
Põhimõtteliselt saate seda kasutada mis tahes projekti jaoks, seega pole vahet, millise töökoormuse seadistate. Kuid seekord kui "individuaalne komponent", ". NET kompilaatori platvorm SDK. See on kasulik silumiseks allikageneraatori arendamise ajal. Kui teil on Visual Studio juba installitud, saate selle lisada Visual Studio menüüst jaotises Tööriistad > hangi tööriistad ja funktsioonid.
Allikageneraatori projekti loomine ja ettevalmistamine
Source Generator luuakse põhirakenduse projektist eraldi projektis. Pole tähtis, kas loote need kõigepealt või loote hiljem täiendavaid. Sel juhul loon selle allikageneraatori projektist.
Kuval Uue projekti loomine valige Klassiteek.
Projekti nimi võib olla ükskõik milline, kuid praegu CodeGenerator
jätame selle .
Klassiteekide puhul toetab allikageneraator praegu . NET Standard 2.0.
Kui olete oma projekti loonud, hankige pakett NuGeti abil Microsoft.CodeAnalysis.CSharp
.
Käitumine võib sõltuvalt versioonist erineda, kuid vana versiooni kasutamist pole mõtet jätkata, seega panen uusima versiooni.
Seejärel avage projektifail koodina.
Kui avate selle, näete järgmist.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
</ItemGroup>
</Project>
Lisage see järgmiselt: Lisage julgelt midagi muud, mida vajate.
<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>
Seejärel kirjutage selle klassi kood, mis koodi automaatselt genereerib.
Kõigepealt loome ainult raamistiku, nii et palun kirjutage olemasolev kood algusest peale Class1.cs
ümber või lisage uus kood.
Kood peaks välja nägema selline: Klassi nimi võib olla midagi, kuid parem on nimi, mis näitab, millist koodi automaatselt genereeritakse.
SampleGenerator
Praegu jätke see nii .
Initialize
Selles meetodis kirjutate koodianalüüsi ja koodi genereerimise protsessi.
using Microsoft.CodeAnalysis;
namespace CodeGenerator
{
[Generator(LanguageNames.CSharp)]
public partial class SampleGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
}
}
}
Seejärel looge rakenduse jaoks projekt, mille jaoks soovite koodi automaatselt genereerida. See võib olla ükskõik milline, kuid siin kasutame seda lihtsalt konsoolirakendusena. Selle projekti raamistiku tüüp ja versioon, millele kood automaatselt genereeritakse, vastab üldnumbrile.
Lisage viide lähtegeneraatori projektile rakenduspoolsest projektist.
Mõningaid seadeid ei saa omadustes seadistada, seega avage projektifail koodis.
Ma arvan, et see näeb välja selline:
<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>
Lisage viidatud projekti sätted järgmiselt. Veenduge, et te ei tee XML-i süntaksis viga.
<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>
Seejärel avage projekti omadused "Koodigeneraatori poolel".
Klõpsake linki, et avada silumise käivitamise atribuutide kasutajaliides.
Kustutage algne profiil, kuna te ei soovi seda kasutada.
Lisage uus profiil.
Valige Roslyni komponent.
Kui olete seni seaded teinud, peaksite saama valida rakenduse projekti, nii et valige see.
See on ettevalmistuse lõpp.
Kontrollige, kas saate siluda
Avage lähtegeneraatori kood ja Initialize
asetage meetodi lõppu murdepunkt.
Silume allika generaatori.
Kui protsess peatub murdepunktis, saate kinnitada, et silute normaalselt. See peaks muutma teie allikageneraatori arendamise mõistlikult lihtsaks.
Praegu väljastame fikseeritud koodi
Esiteks proovime lihtsalt väljastada fikseeritud koodi. See on lihtne, sest te ei pea isegi koodi analüüsima. Isegi kui see on fikseeritud kood, käsitletakse seda stringina, seega on programmi loomisega võimalik koodi tootmist fikseeritud kujul suurendada.
Kui soovite väljastada fikseeritud koodi, saate context.RegisterPostInitializationOutput
seda teha, kasutades .
Järgnevalt on toodud näide koodi väljundist.
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));
});
}
}
Koodi sisu on nagu kommentaarides kirjutatud, seega jätan üksikasjad välja. Ehitage see ja veenduge, et vigu pole.
Kui ehitamine on lõpule jõudnud, saate rakenduse projektis laiendada "Analüsaatorid", et näha koodigeneraatori genereeritud koodi.
Kui te seda ei näe, taaskäivitage Visual Studio ja kontrollige seda. Tundub, et praegu ei pruugi Visual Studio värskendada, kui te seda ei taaskäivita. Programmide loomisel kasutatavad Intellisence ja süntaksi esiletõstmine on sarnased. Kuna aga kood ise kajastub ehitamise ajal, tundub, et programm kajastub rakenduse poolel.
Kui kood on loodud, proovige seda oma rakenduses kasutada. See peaks hästi toimima.
Koodi analüüsimine ja genereerimine
Kui töötlete koodi lihtsalt normaalselt ja väljastate selle, ei erine see palju teistest automaatsetest koodi genereerimistest ning te ei saa lähtegeneraatori eeliseid ära kasutada. Nüüd analüüsime rakenduse projekti koodi ja genereerime koodi vastavalt.
Koodi analüüs on siiski üsna sügav, nii et ma ei saa siin kõike seletada. Praegu selgitan kuni punktini, kus saate koodi analüüsida ja väljastada. Kui soovite minna sügavamale, palun tehke oma uurimistööd.
Praegu tahaksin näiteks automaatselt luua koodi "lisada meetod kõigile loodud klassidele Reset
ja lähtestada kõigi omaduste väärtus default
".
Viimastel aastatel tundub, et eelistatud on muutumatute juhtumite käsitlemine, kuid mänguprogrammides ei ole võimalik igal kaadril uut eksemplari luua, seega arvan, et väärtuse lähtestamise protsessi kasutatakse.
default
Võib-olla soovite seada midagi muud, kuid kui te seda teete, on see pikk esimene näide, nii et palun rakendage seda ise.
Koodigeneraatori pool
Looge uus klass, et säilitada varem loodud generaator. Kui automaatselt genereeritav sisu muutub, on parem luua uus klass teises klassis.
Roslyni kompilaator hoolitseb koodi struktureerimise eest, nii et see, mida me siin teeme, on struktureeritud andmete sõelumine ja koodi kirjutamine.
Esiteks postitan kogu koodi. Olen proovinud võimalikult vähe koodi saada. Seekord see töötab, kuid see on ainult tasemel, mis liigub hr/pr.-na, nii et kui te tegelikult arendamisega jätkate, on üsna palju puuduvaid osi. Palun parandage seda vastavalt vajadusele.
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);
});
}
}
Olen sellest kommentaarides kirjutanud, kuid selgitan seda mõnes kohas.
// 構文を解析し処理対象のノードをコード出力用に変換します
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: ...,
// 対象のノードをコード出力に必要な形に変換します
transform: ...
);
context.SyntaxProvider.CreateSyntaxProvider
analüüsida koodi rakendusprojektis ja struktureerida see nii palju kui võimalik.
Kõik on jagatud sõlmedeks ja me kasutame selleks, et teha kindlaks, millist neist me predicate
tahame töödelda.
Vajadusel transform
ka teisendage töödeldud objekt koos ja edastage see väljundi poolele.
predicate
Sel juhul teeme järgmist.
// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: static (node, cancelToken) =>
{
// 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
cancelToken.ThrowIfCancellationRequested();
// クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
// また partial class であること
return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},
Esiteks, nagu iga protsessi puhul, saab koodi automaatse genereerimise protsessi alati enneaegselt tühistada.
Nii et kasutage helistamiseks ThrowIfCancellationRequested
tühistamismärki, et saaksite igal ajal katkestada.
predicate
Nüüd, kui iga sõlme kutsutakse, tahame kindlaks teha, milline neist on see, mida tahame töödelda.
Kuna neid on tohutult palju, on parem neid siin mingil määral kitsendada.
Kuna lisame seekord klassile töötlemise, otsustame, kas see on nii, ja otsustame ClassDeclarationSyntax
, kas töödeldakse ainult klassi.
partial class
Samuti, kuna kood on lisatud , pannakse see partial class
kohtuotsuseks.
// 対象のノードをコード出力に必要な形に変換します
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
võimaldab teil teisendada analüüsi soovitud vormi ja edastada see koodi väljundisse.
Seekord ei tee me palju teisendamist, vaid edastame väärtuse väljundi poolele sellisena, nagu see on return
.
Me saame teel "deklareeritud sümboleid", kuidSemanticModel
me saame palju lisateavet, kasutades ja Syntac
, nii et ma arvan, et saame selle vajadusel kätte.
return
Tuples luuakse ja tagastatakse, kuid väljundi poolele edastatavate andmete konfiguratsioon võib olla midagi.
Kui soovite edastada mitu andmet, saate luua sellise tuple'i või määratleda oma klassi ja edastada selle sisse.
// 解析し変換した情報をもとにコードを出力します
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
Nüüd genereerime ja väljastame koodi töödeldavate andmete põhjal.
context.SyntaxProvider.CreateSyntaxProvider
return
Väärtuse saab vastu võtta teise argumendina Action
, nii et kood genereeritakse selle väärtuse põhjal.
Sel juhul loome koodi, et saada klassi süntaksist atribuutide süntaksiloend ja seada default
atribuudiks igast nimest.
Seejärel looge klassi nimel põhinev meetod Reset
, manustage varem loodud atribuutide loendi kood ja seadke kõigi atribuutide väärtus default
tagasi Reset
väärtusele Meetod on lõpule viidud.
Kood väljastatakse contextSource.AddSource
meetodi iga klassi kohta eraldi.
Muide, põhjus, miks ma panin failinimesse "g", on see, et oleks lihtsam kindlaks teha, kas see on käsitsi loodud kood või automaatselt genereeritud koodiviga, kui on olemas ehitusviga.
Kui te seda tegelikult teete, saate selliseid taotlusi nagu "Ma tahan välja lähtestada", "Ma tahan selle lähtestada muul viisil kui vaikimisi", "Ma tahan lähtestada ainult automaatsed atribuudid". Kui paned need sisse, on juhe pikk, nii et proovige seda ise teha.
Rakenduse projektipool
Seekord lõin koodi, mis laiendab klassi meetodit, nii et loon klassi sobivalt.
Sisu on järgmine, kuid kui on omadusi, saate ülejäänud osa vastavalt vajadusele kasutada.
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}}}";
}
}
Koodi loomisel Reset
pikendatakse meetodit, kuid see ei pruugi Visual Studios kajastuda, seega taaskäivitage Visual Studio sel ajal.
Ma arvan, et näete, et koodi laiendatakse automaatselt analüsaatorile.
Taane on kummaline, kuid võite seda ignoreerida, kuna te põhimõtteliselt ei puuduta automaatselt genereeritud koodi.
Program.cs
Proovige kirjutada kood, et nähaReset
, kas saate meetodile helistada.
Ma arvan, et täitmise tulemused kajastuvad ootuspäraselt.
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();