Usar Visual Studio y el Generador de código fuente para generar código automáticamente

Actualización de la página :
Fecha de creación de la página :

Entorno operativo

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

Prerrequisitos

Visual Studio
  • Funciona incluso con una versión algo más antigua
.RED
  • Funciona incluso con una versión algo más antigua

Al principio

Existen varias técnicas para generar código automáticamente con tus propias definiciones, pero en este artículo te mostraré cómo usar un generador de código fuente. Una de las mayores ventajas de un generador de código fuente es que analiza la estructura del código fuente del proyecto actual y genera nuevo código basado en él. Por ejemplo, al crear una nueva clase, puede hacer que el código se agregue automáticamente para que coincida con la clase. Puede programar el tipo de código que desea generar, por lo que puede crear cualquier forma de generación automática de código que desee.

Básicamente, el código se genera automáticamente en segundo plano y se incorpora al proyecto en segundo plano. Dado que no se genera como un archivo de forma visible, no se utiliza con el fin de reutilizar el código generado automáticamente para fines generales (aunque se puede eliminar copiándolo por el momento). Sin embargo, dado que el código se genera automáticamente de acuerdo con la estructura del proyecto, reduce el costo de la entrada manual y reduce los errores de escritura de código en consecuencia, lo cual es una gran ventaja.

En este artículo, explicaré cómo verificar que el código se genere automáticamente, por lo que no iré tan lejos como para analizar el código en profundidad y realizar una salida avanzada. Por favor, búsquelo usted mismo como una aplicación.

arreglo

En primer lugar, instale Visual Studio. Una breve explicación se resume en los siguientes consejos.

Básicamente, puedes usarlo para cualquier proyecto, por lo que no importa qué carga de trabajo configures. Sin embargo, esta vez, como un "componente individual", ". SDK de la plataforma del compilador de NET. Esto es útil para la depuración durante el desarrollo del generador de código fuente. Si ya tiene instalado Visual Studio, puede agregarlo desde el menú de Visual Studio en Herramientas > Obtener herramientas y características.

Creación y preparación de un proyecto de generador de código fuente

El generador de código fuente se crea en un proyecto independiente del proyecto de aplicación principal. No importa si los creas primero o si creas otros más tarde. En este caso, lo crearé a partir del proyecto Source Generator.

En la pantalla Crear nuevo proyecto, seleccione Biblioteca de clases.

El nombre del proyecto puede ser cualquier cosa, pero por ahora CodeGenerator , lo dejaremos como .

En el caso de las bibliotecas de clases, el Generador de código fuente admite actualmente . Estándar NET 2.0.

Una vez que haya creado el proyecto, obtenga el paquete con Microsoft.CodeAnalysis.CSharp NuGet. El comportamiento puede diferir según la versión, pero no tiene sentido seguir usando la versión anterior, así que pondré la última versión.

A continuación, abra el archivo de proyecto como código.

Cuando lo abras, verás lo siguiente.

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

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

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

</Project>

Añádelo de la siguiente manera: Siéntase libre de agregar cualquier otra cosa que necesite.

<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ón, escriba el código de la clase que generará automáticamente el código. En primer lugar, solo crearemos un marco, así que reescriba el código existente desde el principio Class1.cs o agregue un nuevo código.

El código debería tener el siguiente aspecto: El nombre de la clase puede ser cualquier cosa, pero es mejor tener un nombre que muestre qué tipo de código se genera automáticamente. SampleGenerator Por ahora, déjalo como . Initialize En este método, escribirá el proceso de análisis y generación de código.

using Microsoft.CodeAnalysis;

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

A continuación, cree un proyecto para la aplicación para la que desea generar código automáticamente. Puede ser cualquier cosa, pero aquí simplemente lo usaremos como una aplicación de consola. El tipo y la versión del marco del proyecto al que se genera automáticamente el código corresponde al número general.

Agregue una referencia al proyecto del generador de origen desde el proyecto del lado de la aplicación.

Algunas configuraciones no se pueden establecer en las propiedades, así que abra el archivo de proyecto en el código.

Creo que se ve así:

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

Agregue la configuración del proyecto al que se hace referencia de la siguiente manera: Asegúrese de no cometer un error en la sintaxis del 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ón, abra las propiedades del proyecto en el "Lado del generador de código".

Haga clic en el vínculo para abrir la interfaz de usuario Propiedades de inicio de depuración.

Elimine el perfil original porque no desea usarlo.

Agregue un nuevo perfil.

Seleccione Componente Roslyn.

Si ha realizado la configuración hasta ahora, debería poder seleccionar el proyecto de aplicación, así que selecciónelo.

Este es el final de la preparación.

Compruebe si puede depurar

Abra el código del generador fuente y Initialize coloque un punto de interrupción al final del método.

Depuremos el generador de código fuente.

Si el proceso se detiene en el punto de interrupción, puede confirmar que está depurando normalmente. Esto debería hacer que el desarrollo de su generador de fuentes sea razonablemente fácil.

Por el momento, vamos a generar un código fijo

Primero, intentemos generar un código fijo fácilmente. Es fácil porque ni siquiera necesitas analizar el código. Incluso si se trata de un código fijo, se maneja como una cadena, por lo que es posible aumentar la producción de código en una forma fija mediante la construcción de un programa.

Si desea generar un código fijo, context.RegisterPostInitializationOutput puede hacerlo mediante . A continuación se muestra un ejemplo de la salida del código.

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 contenido del código es el que está escrito en los comentarios, por lo que omitiré los detalles. Constrúyelo y asegúrate de que no haya errores.

Una vez completada la compilación, puede expandir "Analizadores" en el proyecto de aplicación para ver el código generado por el generador de código.

Si no lo ve, reinicie Visual Studio y compruébelo. Parece que en este momento, es posible que Visual Studio no se actualice a menos que lo reinicie. La inteligencia y el resaltado de sintaxis utilizados al crear programas son similares. Sin embargo, dado que el código en sí se refleja en el momento de la compilación, parece que el programa se refleja en el lado de la aplicación.

Una vez generado el código, intente usarlo en la aplicación. Debería funcionar bien.

Analizar y generar código

Si solo procesa el código normalmente y lo genera, no es muy diferente de otra generación automática de código y no puede aprovechar los beneficios del generador de fuentes. Así que ahora analicemos el código del proyecto de aplicación y generemos el código en consecuencia.

Sin embargo, el análisis del código es bastante profundo, por lo que no puedo explicarlo todo aquí. Por el momento, explicaré hasta el punto en que pueda analizar y generar el código. Si quieres profundizar, por favor haz tu propia investigación.

Por el momento, a modo de ejemplo, me gustaría crear automáticamente el código "agregar un método a Reset todas las clases creadas y restablecer el valor default de todas las propiedades a ". En los últimos años, parece que se ha preferido el manejo de instancias inmutables, pero en los programas de juegos, no es posible crear una nueva instancia en cada fotograma, por lo que creo que hay un uso para el proceso de restablecer el valor. default Es posible que desee establecer algo más que eso, pero si lo hace, será un primer ejemplo largo, así que aplíquelo usted mismo.

Lado del generador de código

Cree una nueva clase con el fin de mantener el generador que creó anteriormente. Si el contenido que se va a generar automáticamente cambia, es mejor crear uno nuevo en otra clase.

El compilador Roslyn se encarga de la estructuración del código, por lo que lo que vamos a hacer aquí es analizar los datos estructurados y escribir el código.

Primero, publicaré todo el código. He tratado de tener la menor cantidad de código posible. Esta vez funciona, pero es solo a un nivel que se mueve como Sr./Sra., por lo que cuando realmente proceda con el desarrollo, faltarán bastantes partes. Por favor, mejore allí según sea necesario.

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

He escrito sobre ello en los comentarios, pero lo explicaré en algunos lugares.

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

context.SyntaxProvider.CreateSyntaxProvider para analizar el código en el proyecto de aplicación y estructurarlo tanto como sea posible. Todo está dividido en nodos, y los usamos para determinar cuál de ellos queremos predicate procesar. Además, si es necesario transform , convierta el objeto procesado con y páselo al lado de salida.

predicate En este caso, estamos haciendo lo siguiente.

// すべてのノードが処理対象となるため、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 lugar, como con cualquier proceso, el proceso de generación automática de código siempre se puede cancelar prematuramente. Por lo tanto, use el token de cancelación para llamar y ThrowIfCancellationRequested poder interrumpir en cualquier momento.

predicate Ahora que se llama a cada nodo, queremos determinar cuál es el que queremos procesar. Dado que hay una gran cantidad de ellos, es mejor reducirlos aquí hasta cierto punto.

Dado que esta vez vamos a agregar el procesamiento a la clase, determinaremos ClassDeclarationSyntax si es así y determinaremos si solo se procesará la clase. partial class Además, dado que el código se adjunta con , se partial class pone como un juicio.

// 対象のノードをコード出力に必要な形に変換します
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 Le permite convertir el análisis en la forma requerida y pasarlo a la salida de código. Esta vez, no hacemos mucha conversión, sino que pasamos el valor al lado de salida tal como es return . Estamos obteniendo "símbolos declarados" en el camino, peroSemanticModel podemos obtener mucha información adicional usando y Syntac , así que creo que podemos obtenerla si es necesario. return Las tuplas se crean y se devuelven, pero la configuración de los datos que se pasarán al lado de salida puede ser cualquier cosa. Si desea pasar varios datos, puede crear una tupla como esta, o puede definir su propia clase y pasarla.

// 解析し変換した情報をもとにコードを出力します
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 Ahora, generaremos y generaremos código basado en los datos que procesará . context.SyntaxProvider.CreateSyntaxProvider return El valor de se puede recibir como el segundo argumento de , por lo que el código se genera en función de Action ese valor.

En este caso, estamos creando código para obtener la lista de sintaxis de las propiedades de la sintaxis de la clase y establecer la default propiedad en de cada nombre.

Después de eso, cree un método basado en Reset el nombre de la clase, inserte el código de la lista de propiedades creada anteriormente y vuelva Reset a establecer el valor default de todas las propiedades en El método se ha completado.

El código se contextSource.AddSource genera por separado para cada clase del método. Por cierto, la razón por la que puse "g" en el nombre del archivo es para que sea más fácil determinar si se trata de código creado manualmente o de un error de código generado automáticamente cuando hay un error de compilación.

Si realmente lo hace, recibirá solicitudes como "Quiero restablecer el campo", "Quiero inicializarlo de otra manera que no sea predeterminada", "Quiero restablecer solo las propiedades automáticas". Si los colocas, el cordón será largo, así que trata de hacerlo tú mismo.

Lado del proyecto de aplicación

Esta vez, creé un código que extiende el método de la clase, por lo que crearé una clase de manera adecuada.

El contenido es el siguiente, pero si hay propiedades, puede usar el resto según corresponda.

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étodo se amplía al Reset crear el código, pero es posible que no se refleje en Visual Studio, por lo que debe reiniciar Visual Studio en ese momento. Creo que puedes ver que el código se extiende automáticamente al analizador.

La sangría es extraña, pero puede ignorarla porque básicamente no toca el código generado automáticamente.

Program.cs Intente escribir código para ver Reset si puede llamar al método. Creo que los resultados de la ejecución se reflejan como se esperaba.

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