Usare Visual Studio e il generatore di origine per generare automaticamente il codice

Pagina aggiornata :
Data di creazione della pagina :

Ambiente operativo

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

Prerequisiti

Visual Studio
  • Funziona anche con una versione un po' più vecchia
.RETE
  • Funziona anche con una versione un po' più vecchia

Dapprima

Esistono diverse tecniche per generare automaticamente codice con le proprie definizioni, ma in questo articolo ti mostrerò come utilizzare un generatore di sorgenti. Uno dei maggiori vantaggi di un generatore di sorgenti è che analizza la struttura del codice sorgente del progetto corrente e genera nuovo codice basato su di esso. Ad esempio, quando si crea una nuova classe, è possibile fare in modo che il codice venga aggiunto automaticamente in modo che corrisponda alla classe. È possibile programmare il tipo di codice che si desidera generare, in modo da poter creare qualsiasi forma di generazione automatica del codice desiderata.

Il codice viene essenzialmente generato automaticamente in background e incorporato nel progetto dietro le quinte. Poiché non viene emesso come file in modo visibile, non viene utilizzato allo scopo di riutilizzare il codice generato automaticamente per scopi generali (anche se può essere rimosso copiandolo per il momento). Tuttavia, poiché il codice viene generato automaticamente in base alla struttura del progetto, riduce il costo dell'input manuale e riduce di conseguenza gli errori di scrittura del codice, il che è un enorme vantaggio.

In questo articolo, spiegherò come verificare che il codice venga generato automaticamente, quindi non arriverò al punto di analizzare effettivamente il codice in modo approfondito ed eseguire un output avanzato. Si prega di cercarlo tu stesso come applicazione.

apparecchio

Innanzitutto, installare Visual Studio. Una breve spiegazione è riassunta nei seguenti suggerimenti.

Fondamentalmente, puoi usarlo per qualsiasi progetto, quindi non importa quale carico di lavoro hai impostato. Tuttavia, questa volta, come "componente individuale", ". NET Compiler Platform SDK. Ciò è utile per il debug durante lo sviluppo del generatore di sorgenti. Se Visual Studio è già installato, è possibile aggiungerlo dal menu di Visual Studio in Strumenti > Ottieni strumenti e funzionalità.

Creazione e preparazione di un progetto generatore di origine

Il generatore di origine viene creato in un progetto separato dal progetto dell'applicazione principale. Non importa se li crei prima o creane altri in un secondo momento. In questo caso, lo creerò dal progetto Source Generator.

Nella schermata Crea nuovo progetto selezionare Libreria di classi.

Il nome del progetto può essere qualsiasi cosa, ma per ora CodeGenerator lo lasceremo come .

Per le librerie di classi, Source Generator supporta attualmente . NET Standard 2.0.

Dopo aver creato il progetto, ottenere il pacchetto con Microsoft.CodeAnalysis.CSharp NuGet. Il comportamento può variare a seconda della versione, ma non ha senso continuare a utilizzare la vecchia versione, quindi metterò l'ultima versione.

Aprire quindi il file di progetto come codice.

Quando lo apri, vedrai quanto segue.

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

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

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

</Project>

Aggiungilo come segue: Sentiti libero di aggiungere qualsiasi altra cosa di cui hai bisogno.

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

Successivamente, scrivi il codice per la classe che genererà automaticamente il codice. Prima di tutto, creeremo solo un framework, quindi per favore riscrivi il codice esistente dall'inizio Class1.cs o aggiungi un nuovo codice.

Il codice dovrebbe essere simile al seguente: Il nome della classe può essere qualsiasi cosa, ma è meglio avere un nome che mostri il tipo di codice generato automaticamente. SampleGenerator Per il momento, lascialo come . Initialize In questo metodo si scriverà il processo di analisi e generazione del codice.

using Microsoft.CodeAnalysis;

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

Creare quindi un progetto per l'applicazione per la quale si desidera generare automaticamente il codice. Può essere qualsiasi cosa, ma qui lo useremo semplicemente come applicazione console. Il tipo e la versione del framework del progetto a cui viene generato automaticamente il codice corrispondono al numero generale.

Aggiungere un riferimento al progetto del generatore di origine dal progetto sul lato applicazione.

Alcune impostazioni non possono essere impostate nelle proprietà, quindi aprire il file di progetto nel codice.

Penso che assomigli a questo:

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

Aggiungere le impostazioni per il progetto a cui si fa riferimento come indicato di seguito: Assicurarsi di non commettere errori nella sintassi del codice 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>

Successivamente, apri le proprietà del progetto sul "Lato generatore di codice".

Fare clic sul collegamento per aprire l'interfaccia utente delle proprietà di avvio del debug.

Elimina il profilo originale perché non vuoi usarlo.

Aggiungi un nuovo profilo.

Selezionate Roslyn Component (Roslyn Component).

Se le impostazioni sono state effettuate finora, dovrebbe essere possibile selezionare il progetto dell'applicazione, quindi selezionarlo.

Questa è la fine della preparazione.

Verificare se è possibile eseguire il debug

Aprire il codice del generatore di origine e Initialize inserire un punto di interruzione alla fine del metodo.

Eseguire il debug del generatore di sorgenti.

Se il processo si arresta in corrispondenza del punto di interruzione, è possibile verificare che il debug sia in corso normalmente. Questo dovrebbe rendere lo sviluppo del tuo generatore di sorgenti ragionevolmente facile.

Per il momento, emettiamo un codice fisso

Innanzitutto, proviamo a produrre facilmente un codice fisso. È facile perché non è nemmeno necessario analizzare il codice. Anche se si tratta di un codice fisso, viene gestito come una stringa, quindi è possibile aumentare la produzione di codice in forma fissa costruendo un programma.

Se si desidera generare un codice fisso, è possibile context.RegisterPostInitializationOutput utilizzare . Di seguito è riportato un esempio dell'output del codice.

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

Il contenuto del codice è quello scritto nei commenti, quindi ometterò i dettagli. Compilalo e assicurati che non ci siano errori.

Al termine della compilazione, è possibile espandere "Analizzatori" nel progetto dell'applicazione per visualizzare il codice generato dal generatore di codice.

Se non viene visualizzato, riavviare Visual Studio e controllarlo. Sembra che in questo momento Visual Studio non possa essere aggiornato a meno che non lo si riavvii. L'Intellisence e l'evidenziazione della sintassi utilizzate durante la creazione di programmi sono simili. Tuttavia, poiché il codice stesso viene riflesso al momento della compilazione, sembra che il programma si rifletta sul lato applicazione.

Una volta generato il codice, provare a usarlo nell'applicazione. Dovrebbe funzionare bene.

Analizzare e generare codice

Se si elabora il codice normalmente e lo si esegue, non è molto diverso da altre generazioni di codice automatico e non è possibile sfruttare i vantaggi del generatore di sorgenti. Analizziamo quindi il codice del progetto dell'applicazione e generiamo il codice di conseguenza.

Tuttavia, l'analisi del codice è piuttosto approfondita, quindi non posso spiegare tutto qui. Per il momento, ti spiegherò fino al punto in cui puoi analizzare e produrre il codice. Se vuoi approfondire, fai le tue ricerche.

Per il momento, ad esempio, vorrei creare automaticamente il codice "aggiungi un metodo a Reset tutte le classi create e reimposta il valore default di tutte le proprietà su ". Negli ultimi anni, sembra che la gestione di istanze immutabili sia stata preferita, ma nei programmi di gioco non è possibile creare una nuova istanza ogni fotogramma, quindi penso che ci sia un uso per il processo di ripristino del valore. default Potresti voler impostare qualcosa di diverso da quello, ma se lo fai, sarà un primo esempio lungo, quindi per favore applicalo tu stesso.

Lato generatore di codice

Creare una nuova classe allo scopo di mantenere il generatore creato in precedenza. Se il contenuto da generare automaticamente cambia, è meglio crearne uno nuovo in un'altra classe.

Il compilatore Roslyn si occupa della strutturazione del codice, quindi quello che faremo qui è analizzare i dati strutturati e scrivere il codice.

Per prima cosa, pubblicherò tutto il codice. Ho cercato di avere meno codice possibile. Questa volta funziona, ma è solo a un livello che si muove come un Mr./Ms., quindi quando si procede effettivamente con lo sviluppo, ci saranno un bel po' di parti mancanti. Si prega di migliorare lì se necessario.

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

Ne ho scritto nei commenti, ma lo spiegherò in alcuni punti.

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

context.SyntaxProvider.CreateSyntaxProvider analizzare il codice nel progetto applicativo e strutturarlo il più possibile. Tutto è diviso in nodi e li usiamo per determinare quale di essi vogliamo predicate elaborare. Inoltre, se necessario transform , convertire l'oggetto elaborato con e passarlo al lato di output.

predicate In questo caso, stiamo facendo quanto segue.

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

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

Prima di tutto, come con qualsiasi processo, il processo di generazione automatica del codice può sempre essere annullato prematuramente. Quindi usa il token di annullamento per ThrowIfCancellationRequested chiamare in modo da poter interrompere in qualsiasi momento.

predicate Ora che ogni nodo è stato chiamato, vogliamo determinare quale è quello che vogliamo elaborare. Dal momento che ce ne sono un numero enorme, è meglio restringerli qui in una certa misura.

Poiché questa volta aggiungeremo l'elaborazione alla classe, ClassDeclarationSyntax determineremo se lo è e determineremo se verrà elaborata solo la classe. partial class Inoltre, poiché il codice è allegato con , viene partial class messo come una sentenza.

// 対象のノードをコード出力に必要な形に変換します
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 Consente di convertire l'analisi nel modulo richiesto e di passarla all'output del codice. Questa volta, non facciamo molta conversione, ma passiamo il valore al lato di output così com'è return . Stiamo ottenendo "simboli dichiarati" lungo la strada, maSemanticModel possiamo ottenere molte informazioni aggiuntive usando e Syntac , quindi penso che possiamo ottenerle se necessario. return Le tuple vengono create e restituite, ma la configurazione dei dati da passare al lato di output può essere qualsiasi cosa. Se si desidera passare più dati, è possibile creare una tupla come questa oppure definire una classe personalizzata e passarla.

// 解析し変換した情報をもとにコードを出力します
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 A questo punto, verrà generato e restituito il codice in base ai dati che devono essere elaborati da . context.SyntaxProvider.CreateSyntaxProvider return Il valore di può essere ricevuto come secondo argomento di Action , quindi il codice viene generato in base a tale valore.

In questo caso, stiamo creando codice per ottenere l'elenco di sintassi delle proprietà dalla sintassi della classe e impostare la default proprietà su da ogni nome.

Successivamente, creare un metodo basato sul Reset nome della classe, incorporare il codice dell'elenco di proprietà creato in precedenza e reimpostare il valore default di tutte le proprietà Reset su Il metodo è completato.

Il codice viene contextSource.AddSource restituito separatamente per ogni classe del metodo. A proposito, il motivo per cui ho inserito "g" nel nome del file è per rendere più facile determinare se si tratta di codice creato manualmente o di un errore di codice generato automaticamente quando si verifica un errore di compilazione.

Se lo fai effettivamente, riceverai richieste come "Voglio resettare il campo", "Voglio inizializzarlo in modo diverso da quello predefinito", "Voglio resettare solo le proprietà automatiche". Se li inserisci, il cavo sarà lungo, quindi prova a farlo da solo.

Lato progetto applicativo

Questa volta, ho creato un codice che estende il metodo della classe, quindi creerò una classe in modo appropriato.

Il contenuto è il seguente, ma se sono presenti proprietà, è possibile utilizzare il resto in base alle esigenze.

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

Il metodo viene esteso quando Reset si crea il codice, ma potrebbe non essere riflesso in Visual Studio, quindi riavviare Visual Studio in quel momento. Penso che tu possa vedere che il codice viene automaticamente esteso all'analizzatore.

L'indentazione è strana, ma puoi ignorarla perché fondamentalmente non tocchi il codice generato automaticamente.

Program.cs Provare a scrivere codice per vedere Reset se è possibile chiamare il metodo. Penso che i risultati dell'esecuzione si riflettano come previsto.

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