Koodi automaatseks genereerimiseks kasutage Visual Studio ja Source Generatorit

Lehekülg uuendatud :
Lehe loomise kuupäev :

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