Utilizați Visual Studio și Source Generator pentru a genera automat cod

Pagina actualizată :
Data creării paginii :

Mediu de operare

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

Cerințe preliminare

Visual Studio
  • Funcționează chiar și cu o versiune ceva mai veche
.NET
  • Funcționează chiar și cu o versiune ceva mai veche

La început

Există mai multe tehnici pentru generarea automată a codului cu propriile definiții, dar în acest articol vă voi arăta cum să utilizați un generator de surse. Unul dintre cele mai mari avantaje ale unui generator sursă este că analizează structura codului sursă al proiectului curent și generează cod nou bazat pe acesta. De exemplu, atunci când creați o clasă nouă, o puteți face astfel încât codul să fie adăugat automat pentru a se potrivi cu clasa. Puteți programa ce fel de cod doriți să generați, astfel încât să puteți crea orice formă de generare automată a codului doriți.

Codul este în esență generat automat în fundal și încorporat în proiect în spatele scenei. Deoarece nu este afișat ca fișier într-un mod vizibil, nu este utilizat în scopul reutilizării codului generat automat în scopuri generale (deși poate fi eliminat prin copierea acestuia pentru moment). Cu toate acestea, deoarece codul este generat automat în funcție de structura proiectului, acesta reduce costul introducerii manuale și reduce erorile de scriere a codului în consecință, ceea ce reprezintă un avantaj imens.

În acest articol, voi explica cum să verific dacă codul este generat automat, așa că nu voi merge atât de departe încât să analizez profund codul și să efectuez o ieșire avansată. Vă rugăm să o căutați singur ca aplicație.

Instalare

Mai întâi, instalați Visual Studio. O scurtă explicație este rezumată în următoarele sfaturi.

Practic, îl puteți folosi pentru orice proiect, deci nu contează ce volum de lucru configurați. Cu toate acestea, de data aceasta, ca o "componentă individuală", ". SDK-ul platformei compilatorului NET. Acest lucru este util pentru depanare în timpul dezvoltării generatorului de surse. Dacă aveți deja instalat Visual Studio, îl puteți adăuga din meniul Visual Studio sub Instrumente > Obțineți instrumente și caracteristici.

Crearea și pregătirea unui proiect generator de surse

Generatorul de surse este creat într-un proiect separat de proiectul principal al aplicației. Nu contează dacă le creați mai întâi sau creați altele suplimentare mai târziu. În acest caz, îl voi crea din proiectul Source Generator.

Pe ecranul Creare proiect nou, selectați Bibliotecă clasă.

Numele proiectului poate fi orice, dar deocamdată CodeGenerator îl vom lăsa ca .

Pentru bibliotecile de clase, Generatorul de surse acceptă în prezent . Standardul NET 2.0.

După ce ați creat proiectul, obțineți pachetul cu Microsoft.CodeAnalysis.CSharp NuGet. Comportamentul poate diferi în funcție de versiune, dar nu are rost să continuați să utilizați versiunea veche, așa că voi pune cea mai recentă versiune.

Apoi deschideți fișierul proiectului ca cod.

Când îl deschideți, veți vedea următoarele.

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

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

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

</Project>

Adăugați-l după cum urmează: Simțiți-vă liber să adăugați orice altceva aveți nevoie.

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

Apoi, scrieți codul pentru clasa care va genera automat codul. În primul rând, vom crea doar un cadru, așa că vă rugăm să rescrieți codul existent de la început Class1.cs sau să adăugați un cod nou.

Codul ar trebui să arate astfel: Numele clasei poate fi orice, dar este mai bine să aveți un nume care să arate ce fel de cod este generat automat. SampleGenerator Deocamdată, lăsați-l ca . Initialize În această metodă, veți scrie analiza codului și procesul de generare a codului.

using Microsoft.CodeAnalysis;

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

Apoi, creați un proiect pentru aplicația pentru care doriți să generați automat cod. Poate fi orice, dar aici îl vom folosi pur și simplu ca aplicație de consolă. Tipul și versiunea cadrului proiectului la care este generat automat codul corespunde numărului general.

Adăugați o referință la proiectul generator sursă din proiectul pe partea aplicației.

Unele setări nu pot fi setate în proprietăți, deci deschideți fișierul proiectului în cod.

Cred că arată astfel:

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

Adăugați setările pentru proiectul la care se face referire după cum urmează: Asigurați-vă că nu faceți o greșeală în sintaxa 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>

Apoi, deschideți proprietățile proiectului pe partea "Generator de coduri".

Faceți clic pe link pentru a deschide interfața cu utilizatorul Proprietăți lansare depanare.

Ștergeți profilul inițial deoarece nu doriți să îl utilizați.

Adăugați un profil nou.

Selectați componenta Roslyn.

Dacă ați făcut setările până acum, ar trebui să puteți selecta proiectul aplicației, deci selectați-l.

Acesta este sfârșitul pregătirii.

Verificați dacă puteți depana

Deschideți codul generatorului sursă și Initialize plasați un punct de întrerupere la sfârșitul metodei.

Să depanăm generatorul sursă.

Dacă procesul se oprește la punctul de întrerupere, puteți confirma că depanați normal. Acest lucru ar trebui să facă dezvoltarea generatorului sursă destul de ușoară.

Pentru moment, să scoatem un cod fix

În primul rând, să încercăm să scoatem cu ușurință un cod fix. Este ușor pentru că nici măcar nu trebuie să analizați codul. Chiar dacă este un cod fix, acesta este tratat ca un șir, astfel încât este posibilă creșterea producției de cod într-o formă fixă prin construirea unui program.

Dacă doriți să scoateți un cod fix, puteți context.RegisterPostInitializationOutput face acest lucru utilizând . Următorul este un exemplu de ieșire a codului.

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

Conținutul codului este așa cum este scris în comentarii, așa că voi omite detaliile. Construiți-l și asigurați-vă că nu există erori.

Când compilarea este completă, puteți extinde "Analizoare" în proiectul aplicației pentru a vedea codul generat de generatorul de cod.

Dacă nu îl vedeți, reporniți Visual Studio și verificați-l. Se pare că în acest moment, Visual Studio nu poate fi actualizat decât dacă îl reporniți. Intellisența și evidențierea sintaxei utilizate la crearea programelor sunt similare. Cu toate acestea, deoarece codul în sine este reflectat în momentul construirii, se pare că programul se reflectă pe partea aplicației.

Odată ce codul este generat, încercați să îl utilizați în aplicația dvs. Ar trebui să funcționeze bine.

Analizați și generați cod

Dacă procesați codul în mod normal și îl scoateți, acesta nu este mult diferit de alte generări automate de cod și nu puteți profita de avantajele generatorului sursă. Deci, acum să analizăm codul proiectului aplicației și să generăm codul în consecință.

Cu toate acestea, analiza codului este destul de profundă, așa că nu pot explica totul aici. Pentru moment, voi explica până la punctul în care puteți analiza și scoate codul. Dacă doriți să mergeți mai adânc, vă rugăm să faceți propriile cercetări.

Pentru moment, de exemplu, aș dori să creez automat codul "adăugați o metodă la Reset toate clasele create și resetați valoarea default tuturor proprietăților la ". În ultimii ani, se pare că manipularea instanțelor imuabile a fost preferată, dar în programele de joc, nu este posibil să se creeze o nouă instanță în fiecare cadru, deci cred că există o utilizare pentru procesul de resetare a valorii. default Poate doriți să setați altceva decât asta, dar dacă faceți acest lucru, va fi un prim exemplu lung, așa că vă rugăm să îl aplicați singur.

Partea generatorului de coduri

Creați o nouă clasă în scopul păstrării generatorului pe care l-ați creat anterior. Dacă conținutul care urmează să fie generat automat se schimbă, este mai bine să creați unul nou într-o altă clasă.

Compilatorul Roslyn se ocupă de structurarea codului, așa că ceea ce vom face aici este să analizăm datele structurate și să scriem codul.

În primul rând, voi posta tot codul. Am încercat să am cât mai puțin cod posibil. De data aceasta funcționează, dar este doar la un nivel care se mișcă ca un domn / doamnă, așa că atunci când continuați efectiv cu dezvoltarea, vor exista destul de multe părți lipsă. Vă rugăm să îmbunătățiți acolo după cum este necesar.

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

Am scris despre asta în comentarii, dar o voi explica în unele locuri.

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

context.SyntaxProvider.CreateSyntaxProvider să analizeze codul din proiectul aplicației și să îl structureze cât mai mult posibil. Totul este împărțit în noduri și folosim pentru a determina pe care dintre ele dorim să le procesăm predicate . De asemenea, dacă este necesar transform , convertiți obiectul procesat cu și treceți-l pe partea de ieșire.

predicate În acest caz, facem următoarele.

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

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

În primul rând, ca în cazul oricărui proces, procesul de generare automată a codului poate fi întotdeauna anulat prematur. Deci, utilizați jetonul de anulare pentru a apela, astfel încât să ThrowIfCancellationRequested puteți întrerupe în orice moment.

predicate Acum că fiecare nod este apelat, vrem să determinăm care este cel pe care dorim să îl procesăm. Deoarece există un număr foarte mare de ele, este mai bine să le restrângem aici într-o oarecare măsură.

Deoarece vom adăuga procesarea la clasă de data aceasta, vom ClassDeclarationSyntax determina dacă este și vom determina dacă numai clasa va fi procesată. partial class De asemenea, deoarece codul este atașat cu , este partial class pus ca o judecată.

// 対象のノードをコード出力に必要な形に変換します
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ă permite să convertiți analiza în forma necesară și să o transmiteți la ieșirea codului. De data aceasta, nu facem prea multe conversii, ci trecem valoarea la partea de ieșire așa cum este return . Primim "simboluri declarate" pe parcurs, darSemanticModel putem obține o mulțime de informații suplimentare folosind și Syntac , așa că cred că le putem obține dacă este necesar. return Tuplele sunt create și returnate, dar configurația datelor care urmează să fie transmise pe partea de ieșire poate fi orice. Dacă doriți să transmiteți mai multe date, puteți crea un tuplu ca acesta sau vă puteți defini propria clasă și o puteți transmite.

// 解析し変換した情報をもとにコードを出力します
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 Acum, vom genera și scoate codul pe baza datelor care urmează să fie procesate de . context.SyntaxProvider.CreateSyntaxProvider return Valoarea lui poate fi primită ca al doilea argument al lui Action , deci codul este generat pe baza acelei valori.

În acest caz, creăm cod pentru a obține lista de sintaxă a proprietăților din sintaxa clasei și setăm default proprietatea la din fiecare nume.

După aceea, creați o metodă bazată pe Reset numele clasei, încorporați codul listei de proprietăți create anterior și setați valoarea default tuturor proprietăților înapoi Reset la Metoda este finalizată.

Codul este contextSource.AddSource emis separat pentru fiecare clasă din metodă. Apropo, motivul pentru care am pus "g" în numele fișierului este de a facilita determinarea dacă este un cod creat manual sau o eroare de cod generată automat atunci când există o eroare de compilare.

Dacă reușiți efectiv, veți primi solicitări precum "Vreau să resetez câmpul", "Vreau să-l inițializez altfel decât implicit", "Vreau să resetez numai proprietățile automate". Dacă le puneți, cablul va fi lung, așa că încercați să îl faceți singur.

Partea proiectului de aplicare

De data aceasta, am creat un cod care extinde metoda clasei, așa că voi crea o clasă în mod corespunzător.

Conținutul este după cum urmează, dar dacă există proprietăți, puteți utiliza restul după caz.

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

Metoda este extinsă atunci când Reset creați codul, dar este posibil să nu se reflecte în Visual Studio, deci reporniți Visual Studio în acel moment. Cred că puteți vedea că codul este extins automat la analizor.

Indentarea este ciudată, dar o puteți ignora, deoarece practic nu atingeți codul generat automat.

Program.cs Încercați să scrieți cod pentru a vedea Reset dacă puteți apela metoda. Cred că rezultatele execuției sunt reflectate așa cum era de așteptat.

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