A Visual Studio és a Source Generator használata kód automatikus létrehozásához

Oldal frissítve :
Oldal létrehozásának dátuma :

Működési környezet

Visual Studio
  • Visual Studio 2022
.HÁLÓ
  • .NET 8.0

Előfeltételek

Visual Studio
  • Még egy kicsit régebbi verzióval is működik
.HÁLÓ
  • Még egy kicsit régebbi verzióval is működik

Először

Számos módszer létezik a kód automatikus generálására saját definíciókkal, de ebben a cikkben megmutatom, hogyan kell használni a forrásgenerátort. A forrásgenerátor egyik legnagyobb előnye, hogy elemzi az aktuális projekt forráskódjának szerkezetét, és ez alapján új kódot generál. Ha például létrehoz egy új osztályt, beállíthatja, hogy a kód automatikusan hozzáadódjon, hogy megfeleljen az osztálynak. Beprogramozhatja, hogy milyen kódot szeretne generálni, így bármilyen automatikus kódgenerálást létrehozhat.

A kód lényegében automatikusan generálódik a háttérben, és a színfalak mögött beépül a projektbe. Mivel nem látható módon kerül kiadásra fájlként, nem használják az automatikusan generált kód általános célokra történő újrafelhasználására (bár másolással egyelőre eltávolítható). Mivel azonban a kód automatikusan generálódik a projekt szerkezetének megfelelően, csökkenti a kézi bevitel költségeit, és ennek megfelelően csökkenti a kódírási hibákat, ami hatalmas előny.

Ebben a cikkben elmagyarázom, hogyan lehet ellenőrizni, hogy a kód automatikusan generálódik-e, így nem megyek olyan messzire, hogy ténylegesen mélyen elemezzem a kódot és fejlett kimenetet végezzek. Kérjük, keresse meg saját maga alkalmazásként.

Beállít

Először telepítse a Visual Studiót. A rövid magyarázatot a következő tippek foglalják össze.

Alapvetően bármilyen projekthez használhatja, így nem számít, hogy melyik számítási feladatot állítja be. Ezúttal azonban "egyedi összetevőként", ". NET Compiler Platform SDK. Ez hasznos lehet a hibakereséshez a forrásgenerátor fejlesztése során. Ha már telepítette a Visual Studiót, hozzáadhatja a Visual Studio menüjéből, az Eszközök > Eszközök és szolgáltatások beszerzése alatt.

Forrásgenerátor projekt létrehozása és előkészítése

A Source Generator a fő alkalmazási projekttől különálló projektben jön létre. Nem számít, hogy először hozza létre őket, vagy később hoz létre továbbiakat. Ebben az esetben a Source Generator projektből hozom létre.

A Create New Project (Új projekt létrehozása) képernyőn válassza a Class Library (Osztálykönyvtár) lehetőséget.

A projekt neve bármi lehet, de egyelőre CodeGenerator meghagyjuk .

Az osztálykönyvtárak esetében a Source Generator jelenleg a következőt támogatja: . NET Standard 2.0.

Miután létrehozta a projektet, szerezze be a csomagot a NuGet segítségével Microsoft.CodeAnalysis.CSharp . A viselkedés a verziótól függően eltérő lehet, de nincs értelme folytatni a régi verzió használatát, ezért a legújabb verziót teszem.

Ezután nyissa meg a projektfájlt kódként.

Amikor megnyitja, a következőket fogja látni.

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

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

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

</Project>

Adja hozzá a következőképpen: Nyugodtan adjon hozzá bármi mást, amire szüksége van.

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

Ezután írja be annak az osztálynak a kódját, amely automatikusan létrehozza a kódot. Először is, csak egy keretrendszert hozunk létre, ezért kérjük, írja át a meglévő kódot az elejétől Class1.cs , vagy adjon hozzá egy új kódot.

A kódnak így kell kinéznie: Az osztálynév bármi lehet, de jobb, ha van egy név, amely megmutatja, hogy milyen kódot generál automatikusan. SampleGenerator Egyelőre hagyja . Initialize Ebben a módszerben megírja a kódelemzési és kódgenerálási folyamatot.

using Microsoft.CodeAnalysis;

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

Ezután hozzon létre egy projektet ahhoz az alkalmazáshoz, amelyhez automatikusan kódot szeretne generálni. Bármi lehet, de itt egyszerűen konzolalkalmazásként fogjuk használni. A projekt keretének típusa és verziója, amelyhez a kód automatikusan generálódik, megfelel az általános számnak.

Adjon hozzá egy hivatkozást a forrásgenerátor projektre az alkalmazásoldali projektből.

Egyes beállítások nem állíthatók be a tulajdonságokban, ezért nyissa meg a projektfájlt kódban.

Azt hiszem, így néz ki:

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

Adja hozzá a hivatkozott projekt beállításait az alábbiak szerint: Ügyeljen arra, hogy ne hibázzon az XML szintaxisában.

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

Ezután nyissa meg a projekt tulajdonságait a "Kódgenerátor oldalon".

Kattintson a hivatkozásra a hibakeresési indítás tulajdonságainak felhasználói felületének megnyitásához.

Törölje az eredeti profilt, mert nem szeretné használni.

Adjon hozzá egy új profilt.

Válassza a Roslyn Component lehetőséget.

Ha eddig elvégezte a beállításokat, akkor képesnek kell lennie arra, hogy kiválassza az alkalmazásprojektet, ezért válassza ki.

Ez az előkészítés vége.

Ellenőrizze, hogy tud-e hibakeresést végezni

Nyissa meg a forrásgenerátor kódját, és Initialize helyezzen egy töréspontot a metódus végére.

Hibakeresés a forrásgenerátorban.

Ha a folyamat leáll a töréspontnál, megerősítheti, hogy a hibakeresés normálisan történik. Ennek meglehetősen egyszerűvé kell tennie a forrásgenerátor fejlesztését.

Egyelőre adjunk ki egy fix kódot

Először próbáljunk meg könnyen kiadni egy rögzített kódot. Könnyű, mert nem is kell elemeznie a kódot. Még akkor is, ha ez egy rögzített kód, karakterláncként kezelik, így lehetőség van a kód előállításának növelésére rögzített formában egy program létrehozásával.

Ha rögzített kódot szeretne kimenetre küldeni, context.RegisterPostInitializationOutput ezt a használatával teheti meg. Az alábbi példa a kód kimenetét mutatja be.

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

A kód tartalma megegyezik a megjegyzésekben leírtakkal, ezért kihagyom a részleteket. Építse fel, és győződjön meg róla, hogy nincsenek hibák.

A build befejezése után kibonthatja az "Analyzers" elemet az alkalmazásprojektben a kódgenerátor által létrehozott kód megtekintéséhez.

Ha nem látja, indítsa újra a Visual Studiót, és ellenőrizze. Úgy tűnik, hogy jelenleg előfordulhat, hogy a Visual Studio csak akkor frissül, ha újraindítja. A programok létrehozásakor használt intelligencia és szintaxis kiemelése hasonló. Mivel azonban maga a kód tükröződik az építés idején, úgy tűnik, hogy a program tükröződik az alkalmazás oldalán.

A kód létrehozása után próbálja meg használni az alkalmazásban. Jól kell működnie.

Kód elemzése és létrehozása

Ha csak normálisan dolgozza fel a kódot, és kiadja, akkor nem sokban különbözik a többi automatikus kódgenerálástól, és nem használhatja ki a forrásgenerátor előnyeit. Tehát most elemezzük az alkalmazásprojekt kódját, és ennek megfelelően generáljuk a kódot.

A kód elemzése azonban meglehetősen mély, így itt nem tudok mindent megmagyarázni. Egyelőre elmagyarázom addig a pontig, ahol elemezheti és kiadhatja a kódot. Ha mélyebbre szeretne menni, kérjük, végezzen saját kutatást.

Egyelőre példaként szeretném automatikusan létrehozni a "metódus hozzáadása az összes létrehozott osztályhoz Reset , és visszaállítom az összes tulajdonság értékét default ". Az utóbbi években úgy tűnik, hogy a megváltoztathatatlan példányok kezelését részesítették előnyben, de a játékprogramokban nem lehet minden képkockán új példányt létrehozni, ezért úgy gondolom, hogy van értelme az érték visszaállításának folyamatának. default Lehet, hogy valami mást szeretne beállítani, de ha ezt teszi, akkor ez egy hosszú első példa lesz, ezért kérjük, alkalmazza saját maga.

Kódgenerátor oldal

Hozzon létre egy új osztályt a korábban létrehozott generátor megtartása céljából. Ha az automatikusan generálandó tartalom megváltozik, jobb, ha egy újat hoz létre egy másik osztályban.

A Roslyn fordító gondoskodik a kód strukturálásáról, így itt a strukturált adatok elemzése és a kód megírása lesz.

Először elküldöm az összes kódot. Megpróbáltam a lehető legkevesebb kódot használni. Ezúttal működik, de csak egy olyan szinten, amely Mr./Ms.-ként mozog, így amikor ténylegesen folytatod a fejlesztést, jó néhány hiányzó rész lesz. Kérjük, szükség szerint javítsa ott.

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

Írtam róla a megjegyzésekben, de néhány helyen elmagyarázom.

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

context.SyntaxProvider.CreateSyntaxProvider elemezze a kódot az alkalmazásprojektben, és a lehető legnagyobb mértékben strukturálja. Minden csomópontokra van osztva, és arra használjuk, hogy meghatározzuk, melyiket predicate akarjuk feldolgozni. Továbbá, ha szükséges transform , konvertálja a feldolgozott objektumot és adja át a kimeneti oldalnak.

predicate Ebben az esetben a következőket tesszük.

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

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

Először is, mint minden folyamat esetében, a kód automatikus generálásának folyamata mindig idő előtt törölhető. Ezért használja a megszakítási tokent a híváshoz ThrowIfCancellationRequested , hogy bármikor megszakíthassa.

predicate Most, hogy minden csomópontot meghívunk, meg akarjuk határozni, hogy melyik az, amelyet fel akarunk dolgozni. Mivel hatalmas számuk van, jobb, ha itt bizonyos mértékig szűkítjük őket.

Mivel ezúttal feldolgozást fogunk hozzáadni az osztályhoz, meghatározzuk, hogy az-e, és meghatározzuk ClassDeclarationSyntax , hogy csak az osztály lesz-e feldolgozva. partial class Továbbá, mivel a kód , partial class ítéletként kerül elhelyezésre.

// 対象のノードをコード出力に必要な形に変換します
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 Lehetővé teszi, hogy az elemzést a kívánt formába konvertálja, és átadja a kód kimenetének. Ezúttal nem sok konverziót végzünk, hanem átadjuk az értéket a kimeneti oldalnak, ahogy van return . Útközben "deklarált szimbólumokat" kapunk, deSemanticModel sok további információt kaphatunk a és Syntac használatával, így azt hiszem, szükség esetén megkaphatjuk. return A rekordok létrejönnek és visszaadódnak, de a kimeneti oldalnak átadandó adatok konfigurációja bármi lehet. Ha több adatot szeretne átadni, létrehozhat egy ehhez hasonló rekordot, vagy meghatározhatja saját osztályát, és átadhatja azt.

// 解析し変換した情報をもとにコードを出力します
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 Most kódot generálunk és adunk ki a feldolgozandó adatok alapján. context.SyntaxProvider.CreateSyntaxProvider return A értéke a második argumentumaként Action fogadható, így a kód ezen érték alapján jön létre.

Ebben az esetben olyan kódot hozunk létre, amely az osztály szintaxisából lekéri a tulajdonságok szintaxislistáját, és minden névből beállítja a default tulajdonságot.

Ezután hozzon létre egy metódust az osztály neve alapjánReset, ágyazza be a korábban létrehozott tulajdonságlista kódját, és állítsa vissza Reset az összes tulajdonság értékét default A metódus befejeződött.

A kód contextSource.AddSource a metódus minden osztályához külön kerül kiírásra. Egyébként azért tettem a "g" -t a fájlnévbe, hogy könnyebb legyen meghatározni, hogy manuálisan létrehozott kód vagy automatikusan generált kódhiba, ha építési hiba van.

Ha valóban elkészíti, akkor olyan kéréseket fog kapni, mint például: "Vissza akarom állítani a mezőt", "Az alapértelmezetttől eltérő inicializálni akarom", "Csak az automatikus tulajdonságokat akarom visszaállítani". Ha behelyezi őket, a kábel hosszú lesz, ezért próbálja meg magad csinálni.

Alkalmazási projekt oldal

Ezúttal egy olyan kódot hoztam létre, amely kibővíti az osztály metódusát, így ennek megfelelően létrehozok egy osztályt.

A tartalom a következő, de ha vannak tulajdonságok, akkor a többit szükség szerint használhatja.

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

A metódus a kód létrehozásakor ki lesz terjesztve Reset , de előfordulhat, hogy nem jelenik meg a Visual Studio, ezért ekkor indítsa újra a Visual Studiót. Azt hiszem, láthatja, hogy a kód automatikusan kibővül az analizátorral.

A behúzás furcsa, de figyelmen kívül hagyhatja, mert alapvetően nem érinti az automatikusan generált kódot.

Program.cs Próbáljon kódot írni, hogy lássaReset, meg tudja-e hívni a metódust. Úgy gondolom, hogy a végrehajtás eredményei a várakozásoknak megfelelően tükröződnek.

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