Utilitzeu el Visual Studio i el generador de fonts per generar codi automàticament

Pàgina actualitzada :
Data de creació de la pàgina :

Entorn operatiu

Estudi visual
  • Estudi visual 2022
.XARXA
  • .NET 8.0

Prerequisits

Estudi visual
  • Funciona fins i tot amb una versió una mica més antiga
.XARXA
  • Funciona fins i tot amb una versió una mica més antiga

Al principi

Hi ha diverses tècniques per generar codi automàticament amb les vostres pròpies definicions, però en aquest article us mostraré com utilitzar un generador de fonts. Un dels majors avantatges d'un generador de fonts és que analitza l'estructura del codi font del projecte actual i genera nou codi basat en ell. Per exemple, quan crees una classe nova, pots fer-la de manera que el codi s'afegeixi automàticament perquè coincideixi amb la classe. Podeu programar quin tipus de codi voleu generar, de manera que podeu crear qualsevol forma de generació automàtica de codi que vulgueu.

El codi es genera essencialment automàticament en segon pla i s'incorpora al projecte entre bastidors. Com que no es surt com a fitxer de manera visible, no s'utilitza amb el propòsit de reutilitzar el codi generat automàticament per a propòsits generals (tot i que es pot eliminar copiant-lo de moment). Tanmateix, com que el codi es genera automàticament segons l'estructura del projecte, redueix el cost de l'entrada manual i redueix els errors d'escriptura de codi en conseqüència, cosa que suposa un gran avantatge.

En aquest article, explicaré com comprovar que el codi es genera automàticament, de manera que no arribaré a analitzar realment el codi profundament i realitzar una sortida avançada. Si us plau, busqueu-lo vosaltres mateixos com a aplicació.

Organització

Primer, instal·leu Visual Studio. Una breu explicació es resumeix en els següents Consells.

Bàsicament, podeu utilitzar-lo per a qualsevol projecte, de manera que no importa la càrrega de treball que configureu. No obstant això, aquesta vegada, com a "component individual", ". SDK de la plataforma de compilació NET. Això és útil per a la depuració durant el desenvolupament del generador de fonts. Si ja teniu instal·lat el Visual Studio, podeu afegir-lo des del menú del Visual Studio a Eines > obtenir eines i funcions.

Creació i preparació d'un projecte generador d'origen

Generador d'origen es crea en un projecte separat del projecte d'aplicació principal. No importa si els creeu primer o en creeu d'altres més després. En aquest cas, el crearé a partir del projecte Generador de fonts.

A la pantalla "Crear nou projecte", selecciona "Biblioteca de classe".

El nom del projecte pot ser qualsevol cosa, però de moment CodeGenerator , ho deixarem com .

Per a biblioteques de classe, el generador de fonts suporta actualment . Estàndard NET 2.0.

Un cop hagis creat el teu projecte, aconsegueix el paquet amb Microsoft.CodeAnalysis.CSharp NuGet. El comportament pot variar segons la versió, però no té sentit continuar utilitzant la versió antiga, així que posaré l'última versió.

A continuació, obriu el fitxer del projecte com a codi.

Quan l'obriu, veureu el següent.

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

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

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

</Project>

Afegiu-lo de la següent manera: No dubteu a afegir qualsevol altra cosa que necessiteu.

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

A continuació, escriviu el codi de la classe que generarà automàticament el codi. En primer lloc, només crearem un marc, així que si us plau, reescriviu el codi existent des del principi Class1.cs o afegiu un codi nou.

El codi hauria de tenir aquest aspecte: El nom de la classe pot ser qualsevol cosa, però és millor tenir un nom que mostri quin tipus de codi es genera automàticament. SampleGenerator De moment, deixeu-ho com . Initialize En aquest mètode, escriuràs el procés d'anàlisi de codi i generació de codi.

using Microsoft.CodeAnalysis;

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

A continuació, creeu un projecte per a l'aplicació per al qual voleu generar codi automàticament. Pot ser qualsevol cosa, però aquí simplement l'utilitzarem com a aplicació de consola. El tipus i versió del marc del projecte al qual es genera automàticament el codi correspon al número general.

Afegiu una referència al projecte generador d'origen des del projecte del costat de l'aplicació.

Alguns paràmetres no es poden establir a les propietats, així que obriu el fitxer del projecte en codi.

Crec que queda així:

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

Afegiu la configuració del projecte referenciat de la manera següent: Assegureu-vos que no cometeu cap error en la sintaxi de l'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>

A continuació, obriu les propietats del projecte al "costat del generador de codi".

Feu clic a l'enllaç per obrir la interfície d'usuari de les propietats d'inici de depuració.

Suprimeix el perfil original perquè no el vols utilitzar.

Afegeix un perfil nou.

Seleccioneu Roslyn Component.

Si heu fet la configuració fins ara, hauríeu de poder seleccionar el projecte de l'aplicació, així que seleccioneu-lo.

Aquest és el final de la preparació.

Comproveu si podeu depurar

Obriu el codi generador d'origen i col·loqueu Initialize un punt d'interrupció al final del mètode.

Depurem el generador d'origen.

Si el procés s'atura al punt d'interrupció, podeu confirmar que esteu depurant normalment. Això hauria de facilitar raonablement el desenvolupament del generador d'origen.

De moment, sortim un codi fix

En primer lloc, intentem emetre un codi fix fàcilment. És fàcil perquè ni tan sols cal analitzar el codi. Fins i tot si es tracta d'un codi fix, es gestiona com una cadena, de manera que és possible augmentar la producció de codi de forma fixa mitjançant la construcció d'un programa.

Si voleu emetre un codi fix, podeu context.RegisterPostInitializationOutput fer-ho utilitzant . A continuació es mostra un exemple de la sortida del codi.

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

El contingut del codi és el que està escrit en els comentaris, així que ometreé els detalls. Creeu-lo i assegureu-vos que no hi hagi errors.

Quan s'hagi completat la compilació, podeu ampliar "Analitzadors" al projecte d'aplicació per veure el codi generat pel generador de codi.

Si no el veieu, reinicieu Visual Studio i comproveu-ho. Sembla que en aquest moment, és possible que Visual Studio no s'actualitzi tret que el reinicieu. La intel·ligència i el ressaltat de sintaxi utilitzats en crear programes són similars. Tanmateix, atès que el codi en si es reflecteix en el moment de la compilació, sembla que el programa es reflecteix al costat de l'aplicació.

Un cop generat el codi, proveu d'utilitzar-lo a la vostra aplicació. Hauria de funcionar bé.

Analitzar i generar codi

Si només processeu el codi normalment i el produïu, no és molt diferent d'altres generacions automàtiques de codi i no podeu aprofitar els avantatges del generador de fonts. Així que ara analitzem el codi del projecte d'aplicació i generem el codi en conseqüència.

No obstant això, l'anàlisi del codi és bastant profund, de manera que no puc explicar-ho tot aquí. De moment, explicaré fins al punt en què es pot analitzar i sortir el codi. Si voleu aprofundir més, feu la vostra pròpia investigació.

De moment, com a exemple, m'agradaria crear automàticament el codi "afegir un mètode a Reset totes les classes creades i restablir el valor default de totes les propietats a ". En els últims anys, sembla que s'ha preferit el maneig d'instàncies immutables, però en els programes de joc, no és possible crear una nova instància cada fotograma, així que crec que hi ha un ús per al procés de restabliment del valor. default És possible que vulgueu establir alguna cosa que no sigui això, però si ho feu, serà un primer exemple llarg, així que apliqueu-lo vosaltres mateixos.

Costat generador de codi

Creeu una classe nova amb el propòsit de mantenir el generador que heu creat abans. Si el contingut a generar automàticament canvia, és millor crear-ne un de nou en una altra classe.

El compilador Roslyn s'encarrega de l'estructuració del codi, de manera que el que farem aquí és analitzar les dades estructurades i escriure el codi.

En primer lloc, publicaré tot el codi. He intentat tenir el mínim codi possible. Aquesta vegada funciona, però només està en un nivell que es mou com a Sr./Ms., de manera que quan realment continueu amb el desenvolupament, faltaran força parts. Si us plau, milloreu-hi segons calgui.

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

Ho he escrit als comentaris, però ho explicaré en alguns llocs.

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

context.SyntaxProvider.CreateSyntaxProvider Analitzar el codi en el projecte d'aplicació i estructurar-lo en la mesura del possible. Tot està dividit en nodes, i ens utilitzem per determinar quin d'ells volem predicate processar. També, si cal transform , convertir l'objecte processat amb i passar-lo al costat de sortida.

predicate En aquest cas, estem fent el següent.

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

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

En primer lloc, com en qualsevol procés, el procés de generació automàtica de codi sempre es pot cancel·lar prematurament. Per tant, utilitzeu el testimoni de cancel·lació per ThrowIfCancellationRequested trucar perquè pugueu interrompre en qualsevol moment.

predicate Ara que cada node està cridat, volem determinar quin és el que volem processar. Com que n'hi ha un gran nombre, és millor reduir-los aquí fins a cert punt.

Com que aquesta vegada afegirem processament a la classe, determinarem si és així i determinarem ClassDeclarationSyntax si només es processarà la classe. partial class A més, com que el codi s'adjunta amb , es partial class posa com a sentència.

// 対象のノードをコード出力に必要な形に変換します
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 Permet convertir l'anàlisi en la forma requerida i passar-la a la sortida del codi. Aquesta vegada, no fem molta conversió, sinó que passem el valor al costat de sortida tal com és return . Estem rebent "símbols declarats" pel camí, peròSemanticModel podem obtenir molta informació addicional utilitzant i Syntac , així que crec que podem obtenir-la si cal. return Els tuples es creen i es retornen, però la configuració de les dades a passar al costat de sortida pot ser qualsevol cosa. Si voleu passar diverses dades, podeu crear un tuple com aquest, o podeu definir la vostra pròpia classe i passar-la.

// 解析し変換した情報をもとにコードを出力します
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 Ara, generarem i sortirem codi a partir de les dades a processar per . context.SyntaxProvider.CreateSyntaxProvider return El valor de es pot rebre com el segon argument de , de manera que el codi es genera a partir d'aquest Action valor.

En aquest cas, estem creant codi per obtenir la llista de sintaxi de propietats de la sintaxi de la classe i establir la default propietat a partir de cada nom.

Després d'això, creeu un mètode basat en Reset el nom de la classe, inseriu el codi de la llista de propietats creada anteriorment i torneu a definir el valor default de totes les propietats Reset a El mètode s'ha completat.

El codi es contextSource.AddSource surt per separat per a cada classe del mètode. Per cert, la raó per la qual poso "g" al nom del fitxer és per facilitar la determinació de si es crea codi manualment o es genera automàticament un error de codi quan hi ha un error de compilació.

Si realment ho feu, rebreu sol·licituds com "Vull restablir el camp", "Vull inicialitzar-lo diferent del predeterminat", "Vull restablir només les propietats automàtiques". Si els poseu, el cable serà llarg, així que intenteu fer-ho vosaltres mateixos.

Costat del projecte d'aplicació

Aquesta vegada, he creat un codi que amplia el mètode de la classe, així crearé una classe adequadament.

El contingut és el següent, però si hi ha propietats, podeu utilitzar la resta segons convingui.

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

El mètode s'amplia quan Reset creeu el codi, però és possible que no es reflecteixi al Visual Studio, així que reinicieu el Visual Studio en aquell moment. Crec que es pot veure que el codi s'estén automàticament a l'analitzador.

La sagnia és estranya, però podeu ignorar-la perquè bàsicament no toqueu el codi generat automàticament.

Program.cs Proveu d'escriure codi per veure Reset si podeu trucar al mètode. Crec que els resultats de l'execució es reflecteixen com s'esperava.

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