Luo koodi automaattisesti Visual Studion ja Source Generatorin avulla

Sivu päivitetty :
Sivun luontipäivämäärä :

Toimintaympäristö

Visuaalinen studio
  • Visuaalinen studio 2022
.VERKKO
  • .NET 8.0

Edellytykset

Visuaalinen studio
  • Se toimii jopa hieman vanhemman version kanssa
.VERKKO
  • Se toimii jopa hieman vanhemman version kanssa

Alun perin

Koodin automaattiseen luomiseen omilla määritelmilläsi on useita tekniikoita, mutta tässä artikkelissa näytän sinulle, kuinka lähdegeneraattoria käytetään. Yksi lähdegeneraattorin suurimmista eduista on, että se analysoi nykyisen projektin lähdekoodin rakenteen ja luo uuden koodin sen perusteella. Kun esimerkiksi luot uuden luokan, voit tehdä siitä niin, että koodi lisätään automaattisesti vastaamaan luokkaa. Voit ohjelmoida, millaista koodia haluat luoda, jotta voit luoda minkä tahansa automaattisen koodin luonnin.

Koodi luodaan olennaisesti automaattisesti taustalla ja sisällytetään projektiin kulissien takana. Koska sitä ei tulosteta tiedostona näkyvällä tavalla, sitä ei käytetä automaattisesti luodun koodin uudelleenkäyttöön yleisiin tarkoituksiin (vaikka se voidaan poistaa kopioimalla se toistaiseksi). Koska koodi luodaan kuitenkin automaattisesti projektin rakenteen mukaan, se vähentää manuaalisen syötön kustannuksia ja vähentää koodin kirjoitusvirheitä vastaavasti, mikä on valtava etu.

Tässä artikkelissa selitän, kuinka tarkistaa, että koodi luodaan automaattisesti, joten en mene niin pitkälle, että analysoisin koodin perusteellisesti ja suorittaisin edistyneen tulostuksen. Etsi se itse sovelluksena.

asennus

Asenna ensin Visual Studio. Lyhyt selitys on tiivistetty seuraavissa vinkeissä.

Periaatteessa voit käyttää sitä mihin tahansa projektiin, joten sillä ei ole väliä, minkä työmäärän määrität. Tällä kertaa "yksittäisenä komponenttina" ". NET-kääntäjän alustan SDK. Tästä on hyötyä virheenkorjauksessa lähdegeneraattorin kehittämisen aikana. Jos sinulla on jo Visual Studio asennettuna, voit lisätä sen Visual Studio -valikosta kohdassa Työkalut > Hae työkalut ja ominaisuudet.

Lähdegeneraattoriprojektin luominen ja valmistelu

Source Generator luodaan pääsovellusprojektista erillisessä projektissa. Sillä ei ole väliä, luotko ne ensin vai luotko lisää myöhemmin. Tässä tapauksessa luon sen Source Generator -projektista.

Valitse Luo uusi projekti -näytössä Luokkakirjasto.

Projektin nimi voi olla mikä tahansa, mutta toistaiseksi CodeGenerator jätämme sen muotoon .

Luokkakirjastoissa Source Generator tukee tällä hetkellä . NET-standardi 2.0.

Kun olet luonut projektisi, hanki paketti NuGetin avulla Microsoft.CodeAnalysis.CSharp . Käyttäytyminen voi vaihdella versiosta riippuen, mutta ei ole mitään järkeä jatkaa vanhan version käyttöä, joten laitan uusimman version.

Avaa sitten projektitiedosto koodina.

Kun avaat sen, näet seuraavan.

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

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

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

</Project>

Lisätään se seuraavasti: Voit vapaasti lisätä mitä tahansa muuta tarvitset.

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

Kirjoita seuraavaksi sen luokan koodi, joka luo koodin automaattisesti. Ensinnäkin luomme vain kehyksen, joten kirjoita olemassa oleva koodi uudelleen alusta alkaen Class1.cs tai lisää uusi koodi.

Koodin pitäisi näyttää tältä: Luokan nimi voi olla mikä tahansa, mutta on parempi, että sinulla on nimi, joka osoittaa, millainen koodi luodaan automaattisesti. SampleGenerator Jätä se toistaiseksi . Initialize Tässä menetelmässä kirjoitat koodianalyysin ja koodinluontiprosessin.

using Microsoft.CodeAnalysis;

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

Luo seuraavaksi projekti sovellukselle, jolle haluat luoda koodin automaattisesti. Se voi olla mitä tahansa, mutta tässä käytämme sitä yksinkertaisesti konsolisovelluksena. Sen projektin kehyksen tyyppi ja versio, johon koodi luodaan automaattisesti, vastaa yleistä numeroa.

Lisää viittaus lähdegeneraattoriprojektiin sovelluspuolen projektista.

Joitakin asetuksia ei voi asettaa ominaisuuksissa, joten avaa projektitiedosto koodissa.

Mielestäni se näyttää tältä:

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

Lisää viitatun projektin asetukset seuraavasti: Varmista, että et tee virhettä XML: n syntaksissa.

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

Avaa seuraavaksi projektin ominaisuudet "Code Generator -puolella".

Napsauta linkkiä avataksesi Debug Launch Properties -käyttöliittymän.

Poista alkuperäinen profiili, koska et halua käyttää sitä.

Lisää uusi profiili.

Valitse Roslyn-komponentti.

Jos olet tehnyt asetukset tähän mennessä, sinun pitäisi pystyä valitsemaan sovellusprojekti, joten valitse se.

Tämä on valmistelun loppu.

Tarkista, voitko virheenkorjauksen

Avaa lähdegeneraattorin koodi ja Initialize aseta keskeytyskohta menetelmän loppuun.

Debugataan lähdegeneraattori.

Jos prosessi pysähtyy keskeytyskohtaan, voit vahvistaa, että teet virheenkorjauksen normaalisti. Tämän pitäisi tehdä lähdegeneraattorin kehittämisestä kohtuullisen helppoa.

Toistaiseksi tulostetaan kiinteä koodi

Yritetään ensin tulostaa kiinteä koodi helposti. Se on helppoa, koska sinun ei tarvitse edes analysoida koodia. Vaikka se on kiinteä koodi, sitä käsitellään merkkijonona, joten koodin tuotantoa kiinteässä muodossa on mahdollista lisätä rakentamalla ohjelma.

Jos haluat tulostaa kiinteän koodin, voit context.RegisterPostInitializationOutput tehdä sen käyttämällä . Seuraavassa on esimerkki koodin tulostamisesta.

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

Koodin sisältö on kuten kommenteissa on kirjoitettu, joten jätän yksityiskohdat pois. Rakenna se ja varmista, ettei virheitä ole.

Kun koontiversio on valmis, voit laajentaa sovellusprojektin "Analysaattorit" nähdäksesi koodigeneraattorin luoman koodin.

Jos et näe sitä, käynnistä Visual Studio uudelleen ja tarkista se. Näyttää siltä, että tällä hetkellä Visual Studiota ei ehkä päivitetä, ellet käynnistä sitä uudelleen. Ohjelmien luomisessa käytetty älykkyys ja syntaksin korostus ovat samanlaisia. Koska koodi itsessään heijastuu kuitenkin rakennushetkellä, näyttää siltä, että ohjelma heijastuu sovelluspuolelle.

Kun koodi on luotu, yritä käyttää sitä sovelluksessasi. Sen pitäisi toimia hyvin.

Analysoi ja luo koodia

Jos käsittelet koodin normaalisti ja lähetät sen, se ei eroa paljon muusta automaattisesta koodinluonnista, etkä voi hyödyntää lähdegeneraattorin etuja. Joten nyt analysoidaan sovellusprojektin koodi ja luodaan koodi vastaavasti.

Koodin analyysi on kuitenkin melko syvä, joten en voi selittää kaikkea täällä. Toistaiseksi selitän siihen pisteeseen asti, jossa voit analysoida ja tulostaa koodin. Jos haluat mennä syvemmälle, tee oma tutkimuksesi.

Toistaiseksi haluaisin esimerkiksi luoda automaattisesti koodin "lisää menetelmä Reset kaikkiin luotuihin luokkiin ja nollaa kaikkien ominaisuuksien arvo default ". Viime vuosina näyttää siltä, että muuttumattomien esiintymien käsittelyä on suosittu, mutta peliohjelmissa ei ole mahdollista luoda uutta esiintymää joka kehykseen, joten mielestäni arvon nollausprosessille on käyttöä. default Haluat ehkä asettaa jotain muuta, mutta jos teet sen, se on pitkä ensimmäinen esimerkki, joten käytä sitä itse.

Koodigeneraattorin puoli

Luo uusi luokka aiemmin luomasi generaattorin säilyttämiseksi. Jos automaattisesti luotava sisältö muuttuu, on parempi luoda uusi toiseen luokkaan.

Roslyn-kääntäjä huolehtii koodin jäsentämisestä, joten aiomme tässä jäsentää strukturoidun datan ja kirjoittaa koodin.

Ensin lähetän kaiken koodin. Olen yrittänyt saada mahdollisimman vähän koodia. Tällä kertaa se toimii, mutta se on vain tasolla, joka liikkuu herrana / rouvana, joten kun todella jatkat kehitystä, puuttuu melko monta osaa. Ole hyvä ja paranna siellä tarpeen mukaan.

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

Olen kirjoittanut siitä kommenteissa, mutta selitän sen joissakin paikoissa.

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

context.SyntaxProvider.CreateSyntaxProvider analysoida sovellusprojektin koodia ja jäsentää sitä mahdollisimman paljon. Kaikki on jaettu solmuihin, ja käytämme määrittämään, mitkä niistä predicate haluamme käsitellä. transform Lisäksi tarvittaessa muunna käsitelty objekti ja välitä se tulostuspuolelle.

predicate Tässä tapauksessa teemme seuraavan.

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

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

Ensinnäkin, kuten minkä tahansa prosessin yhteydessä, koodin automaattinen luontiprosessi voidaan aina peruuttaa ennenaikaisesti. Käytä siis peruutustunnusta soittamiseen ThrowIfCancellationRequested , jotta voit keskeyttää milloin tahansa.

predicate Nyt kun jokaista solmua kutsutaan, haluamme määrittää, mikä niistä on se, jota haluamme käsitellä. Koska niitä on valtava määrä, on parempi kaventaa niitä täällä jossain määrin.

Koska aiomme lisätä käsittelyn luokkaan tällä kertaa, selvitämme ClassDeclarationSyntax , onko se ja määritetäänkö, käsitelläänkö vain luokka. partial class Lisäksi, koska koodi on liitetty , se partial class asetetaan tuomioksi.

// 対象のノードをコード出力に必要な形に変換します
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 Voit muuntaa analyysin vaadittuun muotoon ja välittää sen koodilähtöön. Tällä kertaa emme tee paljon muuntamista, mutta välitämme arvon lähtöpuolelle sellaisenaan return . Saamme "ilmoitettuja symboleja" matkan varrella, muttaSemanticModel voimme saada paljon lisätietoja käyttämällä ja Syntac , joten luulen, että voimme saada sen tarvittaessa. return Tuples luodaan ja palautetaan, mutta lähtöpuolelle välitettävien tietojen kokoonpano voi olla mikä tahansa. Jos haluat välittää useita tietoja, voit luoda tällaisen tuplen tai voit määrittää oman luokan ja välittää sen sisään.

// 解析し変換した情報をもとにコードを出力します
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 Nyt luomme ja tulostamme koodin käsiteltävien tietojen perusteella. context.SyntaxProvider.CreateSyntaxProvider return-arvo voidaan vastaanottaa :n Action toisena argumenttina, joten koodi luodaan tämän arvon perusteella.

Tässä tapauksessa luomme koodin, joka hakee ominaisuuksien syntaksiluettelon luokan syntaksista ja asettaa ominaisuuden arvoksi default kustakin nimestä.

Tämän jälkeen luo menetelmä, joka perustuu Reset luokan nimeen, upota aiemmin luodun ominaisuusluettelon koodi ja aseta kaikkien ominaisuuksien arvo default takaisin Reset arvoon Menetelmä on valmis.

Koodi tulostetaan contextSource.AddSource erikseen kullekin menetelmän luokalle. Muuten, syy siihen, miksi laitoin "g" tiedostonimeen, on helpottaa sen määrittämistä, onko kyseessä manuaalisesti luotu koodi vai automaattisesti luotu koodivirhe, kun on rakennusvirhe.

Jos todella teet sen, saat pyyntöjä, kuten "Haluan nollata kentän", "Haluan alustaa sen muuten kuin oletuksena", "Haluan nollata vain automaattiset ominaisuudet". Jos laitat ne sisään, johto on pitkä, joten yritä tehdä se itse.

Sovellusprojektin puoli

Tällä kertaa loin koodin, joka laajentaa luokan menetelmää, joten luon luokan asianmukaisesti.

Sisältö on seuraava, mutta jos ominaisuuksia on, voit käyttää loput tarpeen mukaan.

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

Menetelmää laajennetaan, kun Reset luot koodin, mutta se ei välttämättä näy Visual Studiossa, joten käynnistä Visual Studio uudelleen tuolloin. Mielestäni näet, että koodi laajenee automaattisesti analysaattoriin.

Sisennys on outo, mutta voit jättää sen huomiotta, koska et periaatteessa koske automaattisesti luotuun koodiin.

Program.cs Yritä kirjoittaa koodi nähdäksesiReset, voitko kutsua menetelmää. Mielestäni toteutuksen tulokset näkyvät odotetusti.

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