Izmantojiet Visual Studio un avota ģeneratoru, lai automātiski ģenerētu kodu

Lapa atjaunota :
Lapas izveides datums :

Darbības vide

Visual Studio
  • Visual Studio 2022
.NETO
  • .NET 8.0

Priekšnoteikumi

Visual Studio
  • Tas darbojas pat ar nedaudz vecāku versiju
.NETO
  • Tas darbojas pat ar nedaudz vecāku versiju

Sākumā

Ir vairākas metodes, kā automātiski ģenerēt kodu ar savām definīcijām, taču šajā rakstā es parādīšu, kā izmantot avota ģeneratoru. Viena no lielākajām avota ģeneratora priekšrocībām ir tā, ka tas analizē pašreizējā projekta pirmkoda struktūru un, pamatojoties uz to, ģenerē jaunu kodu. Piemēram, kad veidojat jaunu klasi, varat to izveidot tā, lai kods tiktu automātiski pievienots atbilstoši klasei. Varat programmēt, kāda veida kodu vēlaties ģenerēt, lai varētu izveidot jebkāda veida automātisko koda ģenerēšanu, kas jums patīk.

Kods būtībā tiek automātiski ģenerēts fonā un iekļauts projektā aizkulisēs. Tā kā tas netiek izvadīts kā fails redzamā veidā, tas netiek izmantots, lai atkārtoti izmantotu automātiski ģenerēto kodu vispārējiem mērķiem (lai gan to var noņemt, pagaidām to kopējot). Tomēr, tā kā kods tiek automātiski ģenerēts atbilstoši projekta struktūrai, tas samazina manuālās ievades izmaksas un attiecīgi samazina koda rakstīšanas kļūdas, kas ir milzīga priekšrocība.

Šajā rakstā es paskaidrošu, kā pārbaudīt, vai kods tiek automātiski ģenerēts, tāpēc es neiešu tik tālu, lai faktiski dziļi analizētu kodu un veiktu uzlabotu izvadi. Lūdzu, uzmeklējiet to pats kā pieteikumu.

Iestatīšanas

Vispirms instalējiet Visual Studio. Īss skaidrojums ir apkopots šajos padomos.

Būtībā to var izmantot jebkuram projektam, tāpēc nav svarīgi, kuru darba slodzi iestatījāt. Tomēr šoreiz kā "atsevišķs komponents", ". NET kompilatora platforma SDK. Tas ir noderīgi atkļūdošanai Source Generator izstrādes laikā. Ja Visual Studio jau ir instalēta, varat to pievienot Visual Studio izvēlnes sadaļā Rīki > Iegūt rīkus un līdzekļus.

Avota ģeneratora projekta izveide un sagatavošana

Source Generator tiek izveidots projektā, kas ir nodalīts no galvenā lietojumprogrammas projekta. Nav svarīgi, vai tos vispirms izveidojat vai vēlāk izveidojat papildu. Šajā gadījumā es to izveidošu no avota ģeneratora projekta.

Ekrānā Jauna projekta izveide atlasiet Klases bibliotēka.

Projekta nosaukums var būt jebkas, bet pagaidām CodeGenerator , mēs to atstāsim kā .

Klašu bibliotēkām Source Generator pašlaik atbalsta . NET standarts 2.0.

Kad esat izveidojis savu projektu, saņemiet paketi ar Microsoft.CodeAnalysis.CSharp NuGet. Uzvedība var atšķirties atkarībā no versijas, taču nav jēgas turpināt izmantot veco versiju, tāpēc es ievietošu jaunāko versiju.

Pēc tam atveriet projekta failu kā kodu.

Atverot to, jūs redzēsiet sekojošo.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
  </ItemGroup>

</Project>

Pievienojiet to šādi: Jūtieties brīvi pievienot visu citu, kas jums nepieciešams.

<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>

Pēc tam uzrakstiet tās klases kodu, kas automātiski ģenerēs kodu. Pirmkārt, mēs izveidosim tikai sistēmu, tāpēc, lūdzu, pārrakstiet esošo kodu no sākuma Class1.cs vai pievienojiet jaunu kodu.

Kodam vajadzētu izskatīties šādi: Klases nosaukums var būt jebkas, bet labāk ir nosaukums, kas parāda, kāda veida kods tiek automātiski ģenerēts. SampleGenerator Pagaidām atstājiet to kā . Initialize Šajā metodē jūs rakstīsit koda analīzes un koda ģenerēšanas procesu.

using Microsoft.CodeAnalysis;

namespace CodeGenerator
{
  [Generator(LanguageNames.CSharp)]
  public partial class SampleGenerator : IIncrementalGenerator
  {
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
    }
  }
}

Pēc tam izveidojiet projektu lietojumprogrammai, kurai vēlaties automātiski ģenerēt kodu. Tas var būt jebkas, bet šeit mēs to vienkārši izmantosim kā konsoles lietojumprogrammu. Projekta ietvara veids un versija, kurai kods tiek ģenerēts automātiski, atbilst vispārējam numuram.

Pievienojiet atsauci uz avota ģeneratora projektu no lietojumprogrammas puses projekta.

Dažus iestatījumus rekvizītos nevar iestatīt, tāpēc atveriet projekta failu kodā.

Es domāju, ka tas izskatās šādi:

<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>

Pievienojiet atsauces projekta iestatījumus šādi: Pārliecinieties, ka nekļūdāties XML sintaksē.

<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>

Pēc tam atveriet projekta rekvizītus "Code Generator pusē".

Noklikšķiniet uz saites, lai atvērtu atkļūdošanas palaišanas rekvizītu lietotāja saskarni.

Izdzēsiet sākotnējo profilu, jo nevēlaties to izmantot.

Pievienojiet jaunu profilu.

Atlasiet Roslyn komponents.

Ja līdz šim esat veicis iestatījumus, jums vajadzētu būt iespējai izvēlēties lietojumprogrammas projektu, tāpēc atlasiet to.

Tas ir sagatavošanās beigas.

Pārbaudiet, vai varat atkļūdot

Atveriet avota ģeneratora kodu un Initialize metodes beigās ievietojiet pārtraukuma punktu.

Atkļūdosim avota ģeneratoru.

Ja process apstājas pie pārtraukuma punkta, varat apstiprināt, ka atkļūdojat normāli. Tam vajadzētu padarīt jūsu avota ģeneratora izstrādi samērā vienkāršu.

Pagaidām izvadīsim fiksētu kodu

Pirmkārt, mēģināsim viegli izvadīt fiksētu kodu. Tas ir viegli, jo jums pat nav jāanalizē kods. Pat ja tas ir fiksēts kods, tas tiek apstrādāts kā virkne, tāpēc ir iespējams palielināt koda ražošanu fiksētā formā, veidojot programmu.

Ja vēlaties izvadīt fiksētu kodu, varat to izdarīt, context.RegisterPostInitializationOutput izmantojot . Tālāk ir sniegts koda izvades piemērs.

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));
    });
  }
}

Koda saturs ir tāds, kā rakstīts komentāros, tāpēc es izlaidīšu detaļas. Veidojiet to un pārliecinieties, ka nav kļūdu.

Kad būvēšana ir pabeigta, lietojumprogrammas projektā varat izvērst "Analizatori", lai redzētu kodu ģeneratora ģenerēto kodu.

Ja to neredzat, restartējiet Visual Studio un pārbaudiet to. Šķiet, ka šobrīd Visual Studio var netikt atjaunināts, ja vien to nerestartējat. Intellisence un sintakses izcelšana, ko izmanto, veidojot programmas, ir līdzīga. Tomēr, tā kā pats kods tiek atspoguļots veidošanas laikā, šķiet, ka programma tiek atspoguļota lietojumprogrammas pusē.

Kad kods ir ģenerēts, mēģiniet to izmantot savā lietojumprogrammā. Tam vajadzētu darboties labi.

Koda analīze un ģenerēšana

Ja jūs vienkārši apstrādājat kodu normāli un izvadāt to, tas nav daudz atšķirīgs no citas automātiskās koda ģenerēšanas, un jūs nevarat izmantot avota ģeneratora priekšrocības. Tāpēc tagad analizēsim lietojumprogrammas projekta kodu un attiecīgi ģenerēsim kodu.

Tomēr koda analīze ir diezgan dziļa, tāpēc es šeit nevaru izskaidrot visu. Pagaidām es paskaidrošu līdz brīdim, kad jūs varat analizēt un izvadīt kodu. Ja vēlaties iedziļināties, lūdzu, veiciet savu pētījumu.

Pagaidām, piemēram, es vēlētos automātiski izveidot kodu "pievienot metodi Reset visām izveidotajām klasēm un atiestatīt visu īpašumu vērtību default uz ". Pēdējos gados šķiet, ka priekšroka tiek dota nemainīgu gadījumu apstrādei, taču spēļu programmās nav iespējams izveidot jaunu gadījumu katrā kadrā, tāpēc es domāju, ka vērtības atiestatīšanas procesam ir pielietojums. default Iespējams, vēlēsities iestatīt kaut ko citu, bet, ja jūs to izdarīsiet, tas būs garš pirmais piemērs, tāpēc, lūdzu, izmantojiet to pats.

Kodu ģeneratora puse

Izveidojiet jaunu klasi, lai saglabātu iepriekš izveidoto ģeneratoru. Ja automātiski ģenerējamais saturs mainās, labāk ir izveidot jaunu citā klasē.

Roslyn kompilators rūpējas par koda strukturēšanu, tāpēc tas, ko mēs šeit darīsim, ir parsēt strukturētos datus un uzrakstīt kodu.

Pirmkārt, es publicēšu visu kodu. Esmu centies, lai būtu pēc iespējas mazāk koda. Šoreiz tas darbojas, bet tas ir tikai tādā līmenī, kas virzās kā Mr./Ms., tāpēc, kad jūs faktiski turpināsiet attīstību, būs diezgan daudz trūkstošo daļu. Lūdzu, uzlabojiet to pēc vajadzības.

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);
    });
  }
}

Esmu par to rakstījis komentāros, bet dažās vietās to paskaidrošu.

// 構文を解析し処理対象のノードをコード出力用に変換します
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
  // すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
  predicate: ...,
  // 対象のノードをコード出力に必要な形に変換します
  transform: ...
);

context.SyntaxProvider.CreateSyntaxProvider analizēt kodu lietojumprogrammas projektā un pēc iespējas strukturēt to. Viss ir sadalīts mezglos, un mēs izmantojam, lai noteiktu, kurus no tiem mēs predicate vēlamies apstrādāt. Tāpat, ja nepieciešams transform , pārveidojiet apstrādāto objektu ar un nododiet to izejas pusē.

predicate Šajā gadījumā mēs rīkojamies šādi.

// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: static (node, cancelToken) =>
{
  // 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
  cancelToken.ThrowIfCancellationRequested();

  // クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
  // また partial class であること
  return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},

Pirmkārt, tāpat kā ar jebkuru procesu, koda automātiskās ģenerēšanas procesu vienmēr var atcelt priekšlaicīgi. Tāpēc izmantojiet atcelšanas marķieri, lai piezvanītu ThrowIfCancellationRequested , lai jebkurā laikā varētu pārtraukt.

predicate Tagad, kad katrs mezgls tiek saukts, mēs vēlamies noteikt, kurš no tiem ir tas, kuru mēs vēlamies apstrādāt. Tā kā to ir ļoti daudz, labāk tos zināmā mērā sašaurināt.

Tā kā mēs šoreiz pievienosim apstrādi klasei, mēs ClassDeclarationSyntax noteiksim, vai tā ir, un noteiksim, vai tiks apstrādāta tikai klase. partial class Arī tāpēc, ka kods ir pievienots ar , tas partial class tiek likts kā spriedums.

// 対象のノードをコード出力に必要な形に変換します
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 ļauj pārvērst analīzi vajadzīgajā formā un nodot to koda izvadei. Šoreiz mēs neveicam daudz reklāmguvumu, bet nododam vērtību izvades pusei, kāda tā ir return . Pa ceļam mēs saņemam "deklarētos simbolus", betSemanticModel mēs varam iegūt daudz papildu informācijas, izmantojot un Syntac , tāpēc es domāju, ka mēs to varam iegūt, ja nepieciešams. return Tuples tiek izveidoti un atgriezti, bet datu konfigurācija, kas jānodod izejas pusē, var būt jebkas. Ja vēlaties nodot vairākus datus, varat izveidot šādu tabulu vai arī definēt savu klasi un nodot to tālāk.

// 解析し変換した情報をもとにコードを出力します
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 Tagad mēs ģenerēsim un izvadīsim kodu, pamatojoties uz datiem, kurus apstrādās . context.SyntaxProvider.CreateSyntaxProvider return Vērtību var saņemt kā otro argumentu no Action , tāpēc kods tiek ģenerēts, pamatojoties uz šo vērtību.

Šajā gadījumā mēs veidojam kodu, lai no klases sintakses iegūtu rekvizītu sintakses sarakstu un iestatītu rekvizītu default no katra nosaukuma.

Pēc tam izveidojiet metodi, kuras pamatā Reset ir klases nosaukums, ieguliet iepriekš izveidotā rekvizītu saraksta kodu un iestatiet visu rekvizītu vērtību default atpakaļ Reset uz Metode ir pabeigta.

Kods tiek contextSource.AddSource izvadīts atsevišķi katrai metodes klasei. Starp citu, iemesls, kāpēc faila nosaukumā ievietoju "g", ir atvieglot noteikt, vai tas ir manuāli izveidots kods vai automātiski ģenerēta koda kļūda, ja ir būvēšanas kļūda.

Ja jūs to patiešām izdarīsit, jūs saņemsit tādus pieprasījumus kā "Es vēlos atiestatīt lauku", "Es vēlos inicializēt to, izņemot noklusējumu", "Es vēlos atiestatīt tikai automātiskos rekvizītus". Ja jūs tos ievietojat, vads būs garš, tāpēc mēģiniet to izdarīt pats.

Lietojumprogrammas projekta puse

Šoreiz es izveidoju kodu, kas paplašina klases metodi, tāpēc es izveidošu klasi atbilstoši.

Saturs ir šāds, bet, ja ir īpašības, pārējo varat izmantot pēc vajadzības.

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}}}";
  }
}

Metode tiek paplašināta, kad Reset izveidojat kodu, taču tā var netikt atspoguļota programmā Visual Studio, tāpēc tajā laikā restartējiet Visual Studio. Es domāju, ka jūs varat redzēt, ka kods tiek automātiski paplašināts uz analizatoru.

Atkāpe ir dīvaina, taču varat to ignorēt, jo būtībā nepieskaraties automātiski ģenerētajam kodam.

Program.cs Mēģiniet ierakstīt kodu, lai redzētuReset, vai varat izsaukt šo metodi. Es domāju, ka izpildes rezultāti tiek atspoguļoti, kā gaidīts.

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