Automatyczne generowanie kodu przy użyciu programu Visual Studio i generatora źródeł

Strona zaktualizowana :
Data utworzenia strony :

Środowisko pracy

Visual Studio
  • informacji o wersji Visual Studio 2022
.SIEĆ
  • .NET 8.0

Warunki wstępne

Visual Studio
  • Działa nawet z nieco starszą wersją
.SIEĆ
  • Działa nawet z nieco starszą wersją

Na początku

Istnieje kilka technik automatycznego generowania kodu z własnymi definicjami, ale w tym artykule pokażę, jak korzystać z Generatora Źródeł. Jedną z największych zalet generatora źródeł jest to, że analizuje on strukturę kodu źródłowego bieżącego projektu i na jego podstawie generuje nowy kod. Na przykład podczas tworzenia nowej klasy można sprawić, że kod zostanie automatycznie dodany w celu dopasowania do klasy. Możesz zaprogramować, jaki rodzaj kodu chcesz wygenerować, dzięki czemu możesz stworzyć dowolną formę automatycznego generowania kodu, którą chcesz.

Kod jest zasadniczo generowany automatycznie w tle i włączany do projektu za kulisami. Ponieważ nie jest on wyprowadzany jako plik w widoczny sposób, nie jest używany do ponownego wykorzystania automatycznie wygenerowanego kodu do celów ogólnych (chociaż na razie można go usunąć, kopiując go). Ponieważ jednak kod jest generowany automatycznie zgodnie ze strukturą projektu, zmniejsza to koszt ręcznego wprowadzania kodu i odpowiednio zmniejsza liczbę błędów w pisaniu kodu, co jest ogromną zaletą.

W tym artykule wyjaśnię, jak sprawdzić, czy kod jest generowany automatycznie, więc nie będę się tak daleko, aby faktycznie dogłębnie przeanalizować kod i wykonać zaawansowane wyjście. Wyszukaj to samodzielnie jako aplikację.

Instalacji

Najpierw zainstaluj program Visual Studio. Krótkie wyjaśnienie podsumowano w poniższych wskazówkach.

Zasadniczo możesz go używać do dowolnego projektu, więc nie ma znaczenia, jakie obciążenie skonfigurujesz. Jednak tym razem, jako "pojedynczy komponent", ". NET Compiler Platform SDK. Jest to przydatne do debugowania podczas programowania generatora źródła. Jeśli masz już zainstalowany program Visual Studio, możesz dodać go z menu programu Visual Studio w obszarze Narzędzia > Pobierz narzędzia i funkcje.

Tworzenie i przygotowywanie projektu generatora źródeł

Generator źródłowy jest tworzony w projekcie odrębnym od głównego projektu aplikacji. Nie ma znaczenia, czy najpierw je utworzysz, czy później stworzysz dodatkowe. W tym przypadku stworzę go z projektu Source Generator.

Na ekranie Utwórz nowy projekt wybierz pozycję Biblioteka klas.

Nazwa projektu może być dowolna, ale na razie CodeGenerator zostawimy ją jako .

W przypadku bibliotek klas Generator źródeł obsługuje obecnie program . NET Standard 2.0.

Po utworzeniu projektu pobierz pakiet za pomocą Microsoft.CodeAnalysis.CSharp narzędzia NuGet. Zachowanie może się różnić w zależności od wersji, ale nie ma sensu dalej korzystać ze starej wersji, więc umieszczę najnowszą wersję.

Następnie otwórz plik projektu jako kod.

Po otwarciu zobaczysz następujące informacje.

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

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

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

</Project>

Dodaj go w następujący sposób: Nie krępuj się dodać wszystkiego, czego potrzebujesz.

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

Następnie napisz kod dla klasy, która automatycznie wygeneruje kod. Przede wszystkim stworzymy tylko framework, więc prosimy o przepisanie istniejącego kodu od początku Class1.cs lub dodanie nowego kodu.

Kod powinien wyglądać następująco: Nazwa klasy może być dowolna, ale lepiej jest mieć nazwę, która pokazuje, jaki rodzaj kodu jest generowany automatycznie. SampleGenerator Na razie zostaw to jako . Initialize W tej metodzie napiszesz proces analizy i generowania kodu.

using Microsoft.CodeAnalysis;

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

Następnie utwórz projekt dla aplikacji, dla której chcesz automatycznie wygenerować kod. Może to być cokolwiek, ale tutaj po prostu użyjemy go jako aplikacji konsolowej. Typ i wersja frameworka projektu, do którego kod jest generowany automatycznie, odpowiada ogólnej liczbie.

Dodaj odwołanie do projektu generatora źródłowego z projektu po stronie aplikacji.

Niektórych ustawień nie można ustawić we właściwościach, więc otwórz plik projektu w kodzie.

Myślę, że wygląda to tak:

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

Dodaj ustawienia dla projektu, do którego odwołuje się odwołanie, w następujący sposób: Upewnij się, że nie popełnisz błędu w składni pliku 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>

Następnie otwórz właściwości projektu po stronie "Generatora kodu".

Kliknij link, aby otworzyć interfejs użytkownika właściwości uruchamiania debugowania.

Usuń oryginalny profil, ponieważ nie chcesz go używać.

Dodaj nowy profil.

Wybierz pozycję Składnik Roslyn.

Jeśli do tej pory dokonałeś ustawień, powinieneś być w stanie wybrać projekt aplikacji, więc wybierz go.

To już koniec przygotowań.

Sprawdź, czy możesz debugować

Otwórz kod generatora źródeł i Initialize umieść punkt przerwania na końcu metody.

Debugujmy generator źródeł.

Jeśli proces zatrzyma się w punkcie przerwania, możesz potwierdzić, że debugujesz normalnie. Powinno to sprawić, że rozwój generatora źródłowego będzie dość łatwy.

Na razie wyprowadźmy stały kod

Najpierw spróbujmy łatwo wyprowadzić stały kod. To proste, ponieważ nie musisz nawet analizować kodu. Nawet jeśli jest to stały kod, jest on obsługiwany jako ciąg znaków, dzięki czemu możliwe jest zwiększenie produkcji kodu w stałej formie poprzez zbudowanie programu.

Jeśli chcesz wyprowadzić stały kod, możesz context.RegisterPostInitializationOutput to zrobić za pomocą . Poniżej znajduje się przykład danych wyjściowych kodu.

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

Treść kodu jest taka, jak napisano w komentarzach, więc pominę szczegóły. Zbuduj go i upewnij się, że nie ma błędów.

Po zakończeniu kompilacji możesz rozwinąć węzeł "Analizatory" w projekcie aplikacji, aby wyświetlić kod wygenerowany przez generator kodu.

Jeśli go nie widzisz, uruchom ponownie program Visual Studio i sprawdź go. Wygląda na to, że w tej chwili program Visual Studio może nie zostać zaktualizowany, chyba że zostanie ponownie uruchomiony. Inteligencja i podświetlanie składni używane podczas tworzenia programów są podobne. Ponieważ jednak sam kod jest odzwierciedlany w momencie kompilacji, wydaje się, że program jest odzwierciedlany po stronie aplikacji.

Po wygenerowaniu kodu spróbuj użyć go w aplikacji. Powinno działać dobrze.

Analizowanie i generowanie kodu

Jeśli po prostu przetworzysz kod normalnie i wyprowadzisz go, nie różni się on zbytnio od innych automatycznych generowania kodu i nie możesz skorzystać z zalet generatora źródła. Przeanalizujmy więc teraz kod projektu aplikacji i odpowiednio go wygenerujmy.

Analiza kodu jest jednak dość głęboka, więc nie jestem w stanie wszystkiego tutaj wyjaśnić. Na razie wyjaśnię do momentu, w którym można przeanalizować i wyprowadzić kod. Jeśli chcesz zagłębić się głębiej, przeprowadź własne badania.

Na razie, jako przykład, chciałbym automatycznie utworzyć kod "dodaj metodę do Reset wszystkich utworzonych klas i zresetuj wartość default wszystkich właściwości do ". W ostatnich latach wydaje się, że preferowana jest obsługa niezmiennych instancji, ale w programach do gier nie jest możliwe tworzenie nowej instancji co klatkę, więc myślę, że istnieje zastosowanie dla procesu resetowania wartości. default Możesz chcieć ustawić coś innego niż to, ale jeśli to zrobisz, będzie to długi pierwszy przykład, więc zastosuj go samodzielnie.

Strona generatora kodu

Utwórz nową klasę w celu zachowania utworzonego wcześniej generatora. Jeśli treść, która ma być generowana automatycznie, ulegnie zmianie, lepiej utworzyć nową w innej klasie.

Kompilator Roslyn zajmuje się strukturyzacją kodu, więc to, co tutaj zrobimy, to przeanalizujemy dane strukturalne i napiszemy kod.

Najpierw zamieszczę cały kod. Starałem się mieć jak najmniej kodu. Tym razem to działa, ale tylko na poziomie, który porusza się jako pan/pani, więc kiedy faktycznie przystąpisz do rozwoju, będzie sporo brakujących części. W razie potrzeby popraw tam.

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

Pisałem o tym w komentarzach, ale w niektórych miejscach wyjaśnię.

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

context.SyntaxProvider.CreateSyntaxProvider aby przeanalizować kod w projekcie aplikacji i ustrukturyzować go w jak największym stopniu. Wszystko jest podzielone na węzły i na podstawie nich określamy predicate , które z nich chcemy przetworzyć. Ponadto, jeśli to konieczne transform , przekonwertuj przetworzony obiekt za pomocą i przekaż go na stronę wyjściową.

predicate W tym przypadku wykonujemy następujące czynności.

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

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

Przede wszystkim, jak w przypadku każdego procesu, proces automatycznego generowania kodu zawsze może zostać przedwcześnie anulowany. Użyj więc tokenu anulowania, aby ThrowIfCancellationRequested zadzwonić, aby móc przerwać w dowolnym momencie.

predicate Teraz, gdy każdy węzeł jest wywoływany, chcemy określić, który z nich jest tym, który chcemy przetworzyć. Ponieważ jest ich ogromna liczba, lepiej je tutaj w pewnym stopniu zawęzić.

Ponieważ tym razem dodamy przetwarzanie do klasy, określimy ClassDeclarationSyntax , czy tak jest i określimy, czy tylko klasa będzie przetwarzana. partial class Ponadto, ponieważ kod jest dołączony do , partial class jest on umieszczany jako osąd.

// 対象のノードをコード出力に必要な形に変換します
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 Umożliwia przekonwertowanie analizy do wymaganej postaci i przekazanie jej do wyjścia kodu. Tym razem nie wykonujemy dużej konwersji, ale przekazujemy wartość na stronę wyjściową taką, jaka jest return . Po drodze otrzymujemy "zadeklarowane symbole", aleSemanticModel możemy uzyskać wiele dodatkowych informacji za pomocą i Syntac , więc myślę, że możemy je uzyskać w razie potrzeby. return Krotki są tworzone i zwracane, ale konfiguracja danych, które mają być przekazywane do strony wyjściowej, może być dowolna. Jeśli chcesz przekazać wiele danych, możesz utworzyć krotkę w ten sposób lub zdefiniować własną klasę i przekazać ją.

// 解析し変換した情報をもとにコードを出力します
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 Teraz wygenerujemy i wyprowadzimy kod na podstawie danych, które mają być przetworzone przez . context.SyntaxProvider.CreateSyntaxProvider return Wartość of może być odebrana jako drugi argument Action , więc kod jest generowany na podstawie tej wartości.

W tym przypadku tworzymy kod, aby pobrać listę składni właściwości ze składni klasy i ustawić default właściwość na from each name.

Następnie utwórz metodę na Reset podstawie nazwy klasy, osadź kod utworzonej wcześniej listy właściwości i ustaw wartość default wszystkich właściwości z powrotem Reset na Metoda została ukończona.

Kod jest contextSource.AddSource wyprowadzany oddzielnie dla każdej klasy w metodzie. Nawiasem mówiąc, powodem, dla którego umieściłem "g" w nazwie pliku, jest ułatwienie określenia, czy jest to kod tworzony ręcznie, czy automatycznie generowany błąd kodu, gdy wystąpi błąd kompilacji.

Jeśli faktycznie to zrobisz, otrzymasz prośby takie jak "Chcę zresetować pole", "Chcę je zainicjować inaczej niż domyślnie", "Chcę zresetować tylko właściwości automatyczne". Jeśli je włożysz, sznurek będzie długi, więc spróbuj zrobić to sam.

Strona projektu aplikacji

Tym razem stworzyłem kod, który rozszerza metodę klasy, więc odpowiednio stworzę klasę.

Zawartość jest następująca, ale jeśli istnieją właściwości, możesz użyć reszty zgodnie z potrzebami.

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 jest rozszerzana podczas Reset tworzenia kodu, ale może nie zostać odzwierciedlona w programie Visual Studio, więc uruchom ponownie program Visual Studio w tym czasie. Myślę, że widać, że kod jest automatycznie rozszerzany na analizator.

Wcięcie jest dziwne, ale możesz je zignorować, ponieważ w zasadzie nie dotykasz automatycznie wygenerowanego kodu.

Program.cs Spróbuj napisać kod, aby sprawdzićReset, czy możesz wywołać metodę. Myślę, że wyniki realizacji są odzwierciedlone zgodnie z oczekiwaniami.

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