Usar o Visual Studio e o gerador de código-fonte para gerar código automaticamente

Página atualizada :
Data de criação de página :

Ambiente operacional

Estúdio Visual
  • Visual Studio 2022
.REDE
  • .NET 8.0

Pré-requisitos

Estúdio Visual
  • Ele funciona mesmo com uma versão um pouco mais antiga
.REDE
  • Ele funciona mesmo com uma versão um pouco mais antiga

Inicialmente

Existem várias técnicas para gerar código automaticamente com suas próprias definições, mas neste artigo mostrarei como usar um gerador de código-fonte. Uma das maiores vantagens de um gerador de código-fonte é que ele analisa a estrutura do código-fonte do projeto atual e gera um novo código baseado nele. Por exemplo, quando você cria uma nova classe, você pode fazê-lo para que o código seja adicionado automaticamente para corresponder à classe. Você pode programar que tipo de código você deseja gerar, para que você possa criar qualquer forma de geração automática de código que você gosta.

O código é essencialmente gerado automaticamente em segundo plano e incorporado ao projeto nos bastidores. Uma vez que não é emitido como um arquivo de forma visível, ele não é usado com a finalidade de reutilizar o código gerado automaticamente para fins gerais (embora possa ser removido copiando-o por enquanto). No entanto, como o código é gerado automaticamente de acordo com a estrutura do projeto, ele reduz o custo de entrada manual e reduz os erros de escrita de código de acordo, o que é uma grande vantagem.

Neste artigo, explicarei como verificar se o código é gerado automaticamente, então não irei tão longe a ponto de realmente analisar o código profundamente e executar a saída avançada. Por favor, procure-o você mesmo como um aplicativo.

configuração

Primeiro, instale o Visual Studio. Uma breve explicação está resumida nas Dicas a seguir.

Basicamente, você pode usá-lo para qualquer projeto, portanto, não importa qual carga de trabalho você configurar. No entanto, desta vez, como um "componente individual", ". NET SDK da plataforma do compilador. Isso é útil para depuração durante o desenvolvimento do Source Generator. Se você já tiver o Visual Studio instalado, poderá adicioná-lo no menu do Visual Studio em Ferramentas > Obter Ferramentas e Recursos.

Criando e preparando um projeto gerador de código-fonte

O Source Generator é criado em um projeto separado do projeto de aplicativo principal. Não importa se você os cria primeiro ou cria outros depois. Neste caso, vou criá-lo a partir do projeto Source Generator.

Na tela Criar Novo Projeto, selecione Biblioteca de Classes.

O nome do projeto pode ser qualquer coisa, mas por enquanto CodeGenerator , vamos deixá-lo como .

Para bibliotecas de classes, o Source Generator atualmente oferece suporte ao . NET Padrão 2.0.

Depois de criar seu projeto, obtenha o pacote com Microsoft.CodeAnalysis.CSharp o NuGet. O comportamento pode diferir dependendo da versão, mas não adianta continuar usando a versão antiga, então vou colocar a versão mais recente.

Em seguida, abra o arquivo de projeto como código.

Ao abri-lo, você verá o seguinte.

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

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

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

</Project>

Adicione-o da seguinte forma: Sinta-se livre para adicionar qualquer outra coisa que você precisa.

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

Em seguida, escreva o código para a classe que irá gerar automaticamente o código. Primeiro de tudo, vamos apenas criar uma estrutura, então reescreva o código existente desde o início Class1.cs ou adicione um novo código.

O código deve ter esta aparência: O nome da classe pode ser qualquer coisa, mas é melhor ter um nome que mostre que tipo de código é gerado automaticamente. SampleGenerator Por enquanto, deixe como . Initialize Neste método, você escreverá a análise de código e o processo de geração de código.

using Microsoft.CodeAnalysis;

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

Em seguida, crie um projeto para o aplicativo para o qual você deseja gerar código automaticamente. Pode ser qualquer coisa, mas aqui vamos simplesmente usá-lo como um aplicativo de console. O tipo e a versão da estrutura do projeto para o qual o código é gerado automaticamente corresponde ao número geral.

Adicione uma referência ao projeto gerador de código-fonte do projeto do lado do aplicativo.

Algumas configurações não podem ser definidas nas propriedades, portanto, abra o arquivo de projeto no código.

Acho que fica assim:

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

Adicione as configurações para o projeto referenciado da seguinte maneira: Certifique-se de que você não cometa um erro na sintaxe do 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>

Em seguida, abra as propriedades do projeto no "lado do gerador de código".

Clique no link para Abrir a interface do usuário Propriedades de inicialização de depuração.

Exclua o perfil original porque você não deseja usá-lo.

Adicione um novo perfil.

Selecione Roslyn Component.

Se você fez as configurações até agora, você deve ser capaz de selecionar o projeto de aplicativo, então selecione-o.

Este é o fim da preparação.

Verifique se você pode depurar

Abra o código-fonte gerador e Initialize coloque um ponto de interrupção no final do método.

Vamos depurar o gerador de código-fonte.

Se o processo parar no ponto de interrupção, você poderá confirmar que está depurando normalmente. Isso deve tornar o desenvolvimento do seu gerador de fontes razoavelmente fácil.

Por enquanto, vamos produzir um código fixo

Primeiro, vamos tentar produzir um código fixo facilmente. É fácil porque você nem precisa analisar o código. Mesmo que seja um código fixo, ele é tratado como uma cadeia de caracteres, então é possível aumentar a produção de código em uma forma fixa construindo um programa.

Se você deseja gerar um código fixo, você context.RegisterPostInitializationOutput pode fazê-lo usando o . A seguir está um exemplo da saída de 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));
    });
  }
}

O conteúdo do código é como escrito nos comentários, por isso vou omitir os detalhes. Compile-o e certifique-se de que não há erros.

Quando a compilação estiver concluída, você pode expandir "Analisadores" no projeto do aplicativo para ver o código gerado pelo gerador de código.

Se você não vê-lo, reinicie o Visual Studio e verifique-o. Parece que, neste momento, o Visual Studio pode não ser atualizado, a menos que você reiniciá-lo. O realce de Intellisence e sintaxe usado ao criar programas são semelhantes. No entanto, uma vez que o código em si é refletido no momento da compilação, parece que o programa é refletido no lado do aplicativo.

Depois que o código for gerado, tente usá-lo em seu aplicativo. Deve funcionar bem.

Analisar e gerar código

Se você apenas processar o código normalmente e produzi-lo, não é muito diferente de outra geração automática de código, e você não pode aproveitar os benefícios do gerador de código-fonte. Então agora vamos analisar o código do projeto de aplicativo e gerar o código de acordo.

No entanto, a análise do código é bastante profunda, então não posso explicar tudo aqui. Por enquanto, vou explicar até o ponto em que você pode analisar e produzir o código. Se você quiser ir mais fundo, por favor, faça sua própria pesquisa.

Por enquanto, como exemplo, gostaria de criar automaticamente o código "adicionar um método a Reset todas as classes criadas e redefinir o valor default de todas as propriedades para ". Nos últimos anos, parece que lidar com instâncias imutáveis tem sido preferido, mas em programas de jogos, não é possível criar uma nova instância a cada quadro, então acho que há um uso para o processo de redefinição do valor. default Você pode querer definir algo diferente disso, mas se você fizer isso, será um longo primeiro exemplo, então por favor, aplique-o você mesmo.

Lado do gerador de código

Crie uma nova classe com a finalidade de manter o gerador que você criou antes. Se o conteúdo a ser gerado automaticamente mudar, é melhor criar um novo em outra classe.

O compilador Roslyn cuida da estruturação do código, então o que vamos fazer aqui é analisar os dados estruturados e escrever o código.

Primeiro, vou postar todo o código. Eu tentei ter o mínimo de código possível. Desta vez funciona, mas é apenas em um nível que se move como um Sr./Ms., então quando você realmente prosseguir com o desenvolvimento, haverá algumas partes faltando. Por favor, melhore lá conforme necessário.

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

Já escrevi sobre isso nos comentários, mas vou explicar em alguns lugares.

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

context.SyntaxProvider.CreateSyntaxProvider para analisar o código no projeto de aplicativo e estruturá-lo tanto quanto possível. Tudo é dividido em nós, e usamos para determinar qual deles queremos predicate processar. Além disso, se necessário transform , converta o objeto processado com e passe-o para o lado de saída.

predicate Neste caso, estamos fazendo o seguinte.

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

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

Em primeiro lugar, como em qualquer processo, o processo de geração automática de código sempre pode ser cancelado prematuramente. Portanto, use o token de cancelamento para ligar para ThrowIfCancellationRequested que você possa interromper a qualquer momento.

predicate Agora que cada nó é chamado, queremos determinar qual é o que queremos processar. Uma vez que há um grande número deles, é melhor restringi-los aqui até certo ponto.

Como vamos adicionar processamento à classe desta vez, determinaremos ClassDeclarationSyntax se é e determinaremos se apenas a classe será processada. partial class Além disso, como o código está anexado ao , ele partial class é colocado como um julgamento.

// 対象のノードをコード出力に必要な形に変換します
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 permite converter a análise no formulário necessário e passá-la para a saída do código. Desta vez, não fazemos muita conversão, mas passamos o valor para o lado da saída como está return . Estamos recebendo "símbolos declarados" ao longo do caminho, masSemanticModel podemos obter muitas informações adicionais usando e Syntac , então acho que podemos obtê-lo se necessário. return Tuplas são criadas e retornadas, mas a configuração dos dados a serem passados para o lado de saída pode ser qualquer coisa. Se você quiser passar vários dados, você pode criar uma tupla como esta, ou você pode definir sua própria classe e passá-lo para dentro.

// 解析し変換した情報をもとにコードを出力します
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 Agora, vamos gerar e emitir código com base nos dados a serem processados pelo . context.SyntaxProvider.CreateSyntaxProvider return O valor de pode ser recebido como o segundo argumento do Action , portanto, o código é gerado com base nesse valor.

Nesse caso, estamos criando código para obter a lista de sintaxe de propriedades da sintaxe da classe e definir a default propriedade como de cada nome.

Depois disso, crie um método com base no Reset nome da classe, incorpore o código da lista de propriedades criada anteriormente e defina o valor default de todas as propriedades de volta Reset para O método é concluído.

O código é contextSource.AddSource gerado separadamente para cada classe no método. By the way, a razão pela qual eu coloquei "g" no nome do arquivo é para tornar mais fácil determinar se é código criado manualmente ou erro de código gerado automaticamente quando há um erro de compilação.

Se você realmente fizer isso, receberá solicitações como "Quero redefinir o campo", "Quero inicializá-lo diferente do padrão", "Quero redefinir apenas as propriedades automáticas". Se você colocá-los, o cordão será longo, então tente fazê-lo sozinho.

Lado do projeto de aplicação

Desta vez, criei um código que estende o método da classe, então vou criar uma classe apropriadamente.

O conteúdo é o seguinte, mas se houver propriedades, você pode usar o resto conforme apropriado.

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

O método é estendido quando Reset você cria o código, mas ele pode não ser refletido no Visual Studio, portanto, reinicie o Visual Studio nesse momento. Acho que você pode ver que o código é automaticamente estendido para o analisador.

O recuo é estranho, mas você pode ignorá-lo porque basicamente não toca no código gerado automaticamente.

Program.cs Tente escrever código para ver Reset se você pode chamar o método. Acho que os resultados da execução estão refletidos como esperado.

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