Uporaba programov Visual Studio in izvorni generator za samodejno ustvarjanje kode

Stran posodobljena :
Datum ustvarjanja strani :

Delovno okolje

Visual Studio
  • Visual Studio 2022
.MREŽA
  • .NET 8.0

Predpogoji

Visual Studio
  • Deluje tudi z nekoliko starejšo različico
.MREŽA
  • Deluje tudi z nekoliko starejšo različico

Sprva

Obstaja več tehnik za samodejno ustvarjanje kode z lastnimi definicijami, vendar vam bom v tem članku pokazal, kako uporabljati generator vira. Ena največjih prednosti generatorja vira je, da analizira strukturo izvorne kode trenutnega projekta in na podlagi nje generira novo kodo. Ko na primer ustvarite nov razred, ga lahko naredite tako, da se koda samodejno doda, da se ujema z razredom. Programirate lahko, katero vrsto kode želite ustvariti, tako da lahko ustvarite katero koli obliko samodejnega ustvarjanja kode, ki vam je všeč.

Koda je v bistvu samodejno ustvarjena v ozadju in vključena v projekt v ozadju. Ker ni izhodna kot datoteka na viden način, se ne uporablja za ponovno uporabo samodejno generirane kode za splošne namene (čeprav jo je za zdaj mogoče odstraniti s kopiranjem). Ker pa se koda samodejno ustvari glede na strukturo projekta, zmanjša stroške ročnega vnosa in ustrezno zmanjša napake pri pisanju kode, kar je velika prednost.

V tem članku bom razložil, kako preveriti, ali je koda samodejno ustvarjena, zato ne bom šel tako daleč, da bi dejansko globoko analiziral kodo in izvedel napredni izhod. Prosimo, da ga sami poiščete kot aplikacijo.

Setup

Najprej namestite Visual Studio. Kratka razlaga je povzeta v naslednjih nasvetih.

V bistvu ga lahko uporabite za kateri koli projekt, zato ni pomembno, katero delovno obremenitev nastavite. Vendar pa tokrat kot "posamezna komponenta", ". SDK platforme prevajalnika NET. To je uporabno za odpravljanje napak med razvojem izvornega generatorja. Če imate že nameščen Visual Studio, ga lahko dodate v meniju Visual Studio v razdelku Orodja > Pridobivanje orodij in funkcij.

Izdelava in priprava projekta izvornega generatorja

Generator vira se ustvari v projektu, ločenem od glavnega aplikativnega projekta. Ni pomembno, ali jih najprej ustvarite ali pozneje ustvarite dodatne. V tem primeru ga bom ustvaril iz projekta Source Generator.

Na zaslonu Ustvarjanje novega projekta izberite Knjižnica razredov.

Ime projekta je lahko karkoli, toda za zdaj CodeGenerator ga bomo pustili kot .

Za knjižnice razredov izvorni generator trenutno podpira . NET standard 2.0.

Ko ustvarite projekt, pridobite paket z Microsoft.CodeAnalysis.CSharp NuGetom. Obnašanje se lahko razlikuje glede na različico, vendar ni smiselno še naprej uporabljati stare različice, zato bom dal najnovejšo različico.

Nato odprite datoteko projekta kot kodo.

Ko ga odprete, boste videli naslednje.

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

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

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

</Project>

Dodajte ga na naslednji način: Dodajte še kaj drugega, kar potrebujete.

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

Nato napišite kodo za razred, ki bo samodejno ustvaril kodo. Najprej bomo ustvarili samo okvir, zato prosimo, da od začetka Class1.cs prepišete obstoječo kodo ali dodate novo kodo.

Koda mora izgledati takole: Ime razreda je lahko karkoli, vendar je bolje imeti ime, ki prikazuje, kakšna koda se samodejno ustvari. SampleGenerator Za zdaj ga pustite kot . Initialize Pri tej metodi boste napisali postopek analize kode in ustvarjanja kode.

using Microsoft.CodeAnalysis;

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

Nato ustvarite projekt za aplikacijo, za katero želite samodejno ustvariti kodo. Lahko je karkoli, toda tukaj ga bomo preprosto uporabili kot konzolno aplikacijo. Vrsta in različica okvira projekta, za katerega se koda samodejno ustvari, ustreza splošni številki.

Dodajte sklic na projekt izvornega generatorja iz projekta na strani aplikacije.

Nekaterih nastavitev v lastnostih ni mogoče nastaviti, zato odprite projektno datoteko v kodi.

Mislim, da izgleda takole:

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

Dodajte nastavitve za referenčni projekt na naslednji način: Prepričajte se, da se v sintaksi XML ne zmotite.

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

Nato odprite lastnosti projekta na strani »Generator kode«.

Kliknite povezavo, da odprete uporabniški vmesnik lastnosti zagona odpravljanja napak.

Izbrišite izvirni profil, ker ga ne želite uporabljati.

Dodajte nov profil.

Izberite Roslyn Component.

Če ste doslej nastavili nastavitve, bi morali imeti možnost izbrati aplikacijski projekt, zato ga izberite.

To je konec priprave.

Preverite, ali lahko odpravite napake

Odprite kodo izvornega generatorja in Initialize postavite prelomno točko na konec metode.

Odpravimo napako v izvornem generatorju.

Če se postopek ustavi na prelomni točki, lahko potrdite, da običajno odpravljate napake. To bi moralo razmeroma olajšati razvoj vašega generatorja vira.

Zaenkrat ustvarimo fiksno kodo

Najprej poskusimo enostavno izpisati fiksno kodo. To je enostavno, ker vam sploh ni treba analizirati kode. Tudi če gre za fiksno kodo, se obravnava kot niz, zato je mogoče z izgradnjo programa povečati proizvodnjo kode v fiksni obliki.

Če želite izpisati fiksno kodo, lahko to storite context.RegisterPostInitializationOutput z uporabo . V nadaljevanju je primer izhodne kode.

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

Vsebina kode je zapisana v komentarjih, zato bom izpustil podrobnosti. Zgradite ga in se prepričajte, da ni napak.

Ko je gradnja končana, lahko v aplikacijskem projektu razširite »Analizatorji«, da si ogledate kodo, ki jo ustvari generator kode.

Če ga ne vidite, znova zaženite Visual Studio in preverite. Zdi se, da trenutno Visual Studio morda ne bo posodobljen, razen če ga znova zaženete. Inteligenca in poudarjanje sintakse, ki se uporabljata pri ustvarjanju programov, sta podobna. Ker pa se sama koda odraža v času izdelave, se zdi, da se program odraža na strani aplikacije.

Ko je koda ustvarjena, jo poskusite uporabiti v aplikaciji. Delovati bi moralo dobro.

Analizirajte in generirajte kodo

Če kodo normalno obdelate in jo izpišete, se ne razlikuje veliko od druge samodejne generacije kode in ne morete izkoristiti prednosti generatorja vira. Zdaj pa analizirajmo kodo prijavnega projekta in ustrezno ustvarimo kodo.

Vendar je analiza kode precej globoka, zato tukaj ne morem razložiti vsega. Zaenkrat bom razložil do točke, ko lahko analizirate in izpišete kodo. Če želite iti globlje, opravite lastno raziskavo.

Zaenkrat bi na primer rad samodejno ustvaril kodo "dodaj metodo Reset vsem ustvarjenim razredom in ponastavil vrednost default vseh lastnosti na ". V zadnjih letih se zdi, da je bilo prednostno ravnanje z nespremenljivimi primerki, vendar v programih iger ni mogoče ustvariti novega primerka vsakega okvirja, zato mislim, da obstaja uporaba za proces ponastavitve vrednosti. default Morda boste želeli nastaviti nekaj drugega kot to, če pa to storite, bo to dolg prvi primer, zato ga uporabite sami.

Stran generatorja kode

Ustvarite nov razred z namenom ohranjanja generatorja, ki ste ga ustvarili prej. Če se vsebina, ki jo je treba samodejno ustvariti, spremeni, je bolje, da ustvarite novo v drugem razredu.

Roslynov prevajalnik skrbi za strukturiranje kode, zato bomo tukaj razčlenili strukturirane podatke in napisali kodo.

Najprej bom objavil vso kodo. Poskušal sem imeti čim manj kode. Tokrat deluje, vendar je le na ravni, ki se premika kot gospod / gospa, tako da, ko boste dejansko nadaljevali z razvojem, bo kar nekaj manjkajočih delov. Prosim, izboljšajte tam, če je potrebno.

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

O tem sem pisal v komentarjih, vendar ga bom ponekod razložil.

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

context.SyntaxProvider.CreateSyntaxProvider analizirati kodo v projektu prijave in jo čim bolj strukturirati. Vse je razdeljeno na vozlišča in uporabljamo za določitev, katero od njih predicate želimo obdelati. Če je potrebno transform , obdelani predmet pretvorite in ga prenesite na izhodno stran.

predicate V tem primeru delamo naslednje.

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

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

Najprej, kot pri vsakem postopku, se lahko postopek samodejnega ustvarjanja kode vedno predčasno prekliče. Zato uporabite žeton za preklic za ThrowIfCancellationRequested klic, da ga lahko kadar koli prekinete.

predicate Zdaj, ko se imenuje vsako vozlišče, želimo ugotoviti, katero je tisto, ki ga želimo obdelati. Ker jih je ogromno, jih je bolje do neke mere zožiti.

Ker bomo tokrat razredu dodali obdelavo, bomo ugotovili, ali je, in ugotovili, ClassDeclarationSyntax ali bo obdelan samo razred. partial class Ker je koda priložena , je partial class postavljena kot sodba.

// 対象のノードをコード出力に必要な形に変換します
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 Omogoča pretvorbo analize v zahtevano obliko in njeno posredovanje izhodni kodi. Tokrat ne naredimo veliko konverzije, ampak vrednost prenesemo na izhodno stran, kot je return . Na poti dobivamo "deklarirane simbole", vendarSemanticModel lahko dobimo veliko dodatnih informacij z uporabo in Syntac , zato mislim, da jih lahko dobimo, če je potrebno. return Tuples se ustvarijo in vrnejo, vendar je konfiguracija podatkov, ki jih je treba prenesti na izhodno stran, lahko karkoli. Če želite posredovati več podatkov, lahko ustvarite takšen tuple ali pa določite svoj razred in ga posredujete.

// 解析し変換した情報をもとにコードを出力します
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 Zdaj bomo ustvarili in izpisali kodo na podlagi podatkov, ki jih bo obdelal . context.SyntaxProvider.CreateSyntaxProvider return Vrednost je mogoče prejeti kot drugi argument Action , zato se koda ustvari na podlagi te vrednosti.

V tem primeru ustvarjamo kodo, da dobimo sintaksni seznam lastnosti iz sintakse razreda in nastavimo default lastnost na vsako ime.

Po tem ustvarite metodo, ki temelji na Reset imenu razreda, vdelajte kodo seznama lastnosti, ki ste ga ustvarili prej, in nastavite vrednost default vseh lastnosti nazaj Reset na Metoda je končana.

Koda se contextSource.AddSource prikaže ločeno za vsak razred v metodi. Mimogrede, razlog, zakaj sem v ime datoteke vnesel "g", je, da lažje ugotovim, ali gre za ročno ustvarjeno kodo ali samodejno ustvarjeno napako kode, ko pride do napake pri gradnji.

Če ga dejansko naredite, boste prejeli zahteve, kot so »Želim ponastaviti polje«, »Želim ga inicializirati, razen privzetega«, »Ponastaviti želim samo samodejne lastnosti«. Če jih vstavite, bo kabel dolg, zato ga poskusite narediti sami.

Stran projekta prijave

Tokrat sem ustvaril kodo, ki razširja metodo razreda, zato bom ustrezno ustvaril razred.

Vsebina je naslednja, če pa obstajajo lastnosti, lahko ostalo uporabite po potrebi.

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

Način je razširjen, ko Reset ustvarite kodo, vendar morda ne bo prikazan v programu Visual Studio, zato znova zaženite Visual Studio v tistem času. Mislim, da lahko vidite, da se koda samodejno razširi na analizator.

Zamik je čuden, vendar ga lahko prezrete, ker se v bistvu ne dotaknete samodejno ustvarjene kode.

Program.cs Poskusite napisati kodo in preveriteReset, ali lahko pokličete metodo. Mislim, da se rezultati usmrtitve odražajo po pričakovanjih.

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