Naudokite "Visual Studio" ir "Source Generator", kad automatiškai sugeneruotumėte kodą

Puslapis atnaujintas :
Puslapio sukūrimo data :

Darbo aplinka

Vizualinė studija
  • "Visual Studio 2022"
.GRYNOJI
  • .NET 8.0

Būtinosios sąlygos

Vizualinė studija
  • Jis veikia net ir su šiek tiek senesne versija
.GRYNOJI
  • Jis veikia net ir su šiek tiek senesne versija

Iš pradžių

Yra keletas būdų, kaip automatiškai generuoti kodą su savo apibrėžimais, tačiau šiame straipsnyje parodysiu, kaip naudoti šaltinio generatorių. Vienas didžiausių šaltinio generatoriaus pranašumų yra tai, kad jis analizuoja dabartinio projekto šaltinio kodo struktūrą ir pagal jį generuoja naują kodą. Pavyzdžiui, kai kuriate naują klasę, galite padaryti ją taip, kad kodas būtų automatiškai pridėtas, kad atitiktų klasę. Galite užprogramuoti, kokį kodą norite generuoti, kad galėtumėte sukurti bet kokią jums patinkančią automatinio kodo generavimo formą.

Kodas iš esmės automatiškai sugeneruojamas fone ir įtraukiamas į projekto užkulisius. Kadangi jis nėra išvestas kaip failas matomu būdu, jis nenaudojamas siekiant pakartotinai naudoti automatiškai sugeneruotą kodą bendriems tikslams (nors kol kas jį galima pašalinti nukopijuojant). Tačiau, kadangi kodas automatiškai generuojamas pagal projekto struktūrą, jis sumažina rankinio įvedimo išlaidas ir atitinkamai sumažina kodo rašymo klaidas, o tai yra didžiulis privalumas.

Šiame straipsnyje paaiškinsiu, kaip patikrinti, ar kodas sugeneruotas automatiškai, todėl neisiu taip toli, kad iš tikrųjų giliai išanalizuočiau kodą ir atlikčiau išplėstinę išvestį. Pažiūrėkite į save kaip į programą.

Sąrankos

Pirmiausia įdiekite "Visual Studio". Trumpas paaiškinimas apibendrintas šiuose patarimuose.

Iš esmės galite jį naudoti bet kuriam projektui, todėl nesvarbu, kokį darbo krūvį nustatėte. Tačiau šį kartą, kaip "individualus komponentas", ". NET kompiliatoriaus platformos SDK. Tai naudinga derinant kuriant "Source Generator". Jei jau įdiegėte "Visual Studio", galite ją įtraukti iš "Visual Studio" meniu skiltyje Įrankiai > Gauti įrankius ir funkcijas.

Šaltinio generatoriaus projekto kūrimas ir paruošimas

Šaltinio generatorius sukuriamas projekte, atskirame nuo pagrindinio programos projekto. Nesvarbu, ar pirmiausia juos sukursite, ar vėliau sukursite papildomų. Tokiu atveju sukursiu jį iš "Source Generator" projekto.

Ekrane Kurti naują projektą pasirinkite Klasės biblioteka.

Projekto pavadinimas gali būti bet koks, bet kol kas CodeGenerator , paliksime jį kaip .

Klasių bibliotekose šaltinio generatorius šiuo metu palaiko . NET standartas 2.0.

Sukūrę projektą, gaukite paketą naudodami Microsoft.CodeAnalysis.CSharp "NuGet". Elgesys gali skirtis priklausomai nuo versijos, tačiau nėra prasmės toliau naudoti senąją versiją, todėl pateiksiu naujausią versiją.

Tada atidarykite projekto failą kaip kodą.

Kai jį atidarysite, pamatysite šiuos dalykus.

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

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

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

</Project>

Pridėkite jį taip: Nedvejodami pridėkite viską, ko jums reikia.

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

Tada parašykite klasės, kuri automatiškai sugeneruos kodą, kodą. Visų pirma, mes sukursime tik sistemą, todėl perrašykite esamą kodą nuo pat pradžių Class1.cs arba pridėkite naują kodą.

Kodas turėtų atrodyti taip: Klasės pavadinimas gali būti bet koks, tačiau geriau turėti pavadinimą, rodantį, koks kodas automatiškai sugeneruojamas. SampleGenerator Kol kas palikite jį kaip . Initialize Šiuo metodu parašysite kodo analizės ir kodo generavimo procesą.

using Microsoft.CodeAnalysis;

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

Tada sukurkite programos, kuriai norite automatiškai generuoti kodą, projektą. Tai gali būti bet kas, bet čia mes tiesiog naudosime ją kaip konsolės programą. Projekto, kuriam kodas automatiškai generuojamas, sistemos tipas ir versija atitinka bendrąjį numerį.

Įtraukite nuorodą į šaltinio generatoriaus projektą iš programos pusės projekto.

Kai kurių nustatymų negalima nustatyti ypatybėse, todėl atidarykite projekto failą kode.

Manau, kad tai atrodo taip:

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

Pridėkite nurodyto projekto parametrus taip: Įsitikinkite, kad nepadarėte klaidos XML sintaksėje.

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

Tada atidarykite projekto ypatybes "Code Generator" pusėje.

Spustelėkite nuorodą, kad atidarytumėte derinimo paleidimo ypatybių vartotojo sąsają.

Ištrinkite pradinį profilį, nes nenorite jo naudoti.

Pridėkite naują profilį.

Pasirinkite Roslyn komponentas.

Jei iki šiol atlikote nustatymus, turėtumėte turėti galimybę pasirinkti programos projektą, todėl pasirinkite jį.

Tai yra paruošimo pabaiga.

Patikrinkite, ar galite derinti

Atidarykite šaltinio generatoriaus kodą ir Initialize metodo pabaigoje įdėkite lūžio tašką.

Derinkime šaltinio generatorių.

Jei procesas sustoja lūžio taške, galite patvirtinti, kad derinate įprastai. Tai turėtų padaryti jūsų šaltinio generatoriaus kūrimą gana lengvą.

Kol kas išveskime fiksuotą kodą

Pirmiausia pabandykime lengvai išvesti fiksuotą kodą. Tai lengva, nes jums net nereikia analizuoti kodo. Net jei tai yra fiksuotas kodas, jis tvarkomas kaip eilutė, todėl galima padidinti kodo gamybą fiksuota forma kuriant programą.

Jei norite išvesti fiksuotą kodą, context.RegisterPostInitializationOutput tai galite padaryti naudodami . Toliau pateikiamas kodo išvesties pavyzdys.

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

Kodo turinys yra toks, koks parašytas komentaruose, todėl praleisiu išsamią informaciją. Sukurkite jį ir įsitikinkite, kad nėra klaidų.

Kai kūrimas bus baigtas, programos projekte galite išplėsti "Analizatorius", kad pamatytumėte kodo generatoriaus sugeneruotą kodą.

Jei jo nematote, iš naujo paleiskite "Visual Studio" ir patikrinkite. Atrodo, kad šiuo metu "Visual Studio" gali būti neatnaujinta, nebent ją paleisite iš naujo. Intellisence ir sintaksės paryškinimas, naudojamas kuriant programas, yra panašus. Tačiau, kadangi pats kodas atsispindi kūrimo metu, atrodo, kad programa atsispindi programos pusėje.

Sugeneravę kodą, pabandykite jį naudoti savo programoje. Tai turėtų veikti gerai.

Analizuokite ir generuokite kodą

Jei tiesiog normaliai apdorojate kodą ir jį išvedate, jis nedaug skiriasi nuo kito automatinio kodo generavimo ir negalite pasinaudoti šaltinio generatoriaus pranašumais. Taigi dabar išanalizuokime programos projekto kodą ir atitinkamai sugeneruokime kodą.

Tačiau kodo analizė yra gana gili, todėl čia negaliu visko paaiškinti. Kol kas paaiškinsiu iki taško, kur galite analizuoti ir išvesti kodą. Jei norite įsigilinti, atlikite savo tyrimus.

Šiuo metu, pavyzdžiui, norėčiau automatiškai sukurti kodą "pridėti metodą prie Reset visų sukurtų klasių ir iš naujo nustatyti visų savybių vertę default į ". Pastaraisiais metais atrodo, kad pirmenybė buvo teikiama nekintamų egzempliorių tvarkymui, tačiau žaidimų programose neįmanoma sukurti naujo egzemplioriaus kiekviename kadre, todėl manau, kad yra naudojamas vertės nustatymo iš naujo procesui. default Galbūt norėsite nustatyti ką nors kita, bet jei tai padarysite, tai bus ilgas pirmasis pavyzdys, todėl pritaikykite jį patys.

Kodų generatoriaus pusė

Sukurkite naują klasę, kad išlaikytumėte anksčiau sukurtą generatorių. Jei automatiškai generuojamas turinys pasikeičia, geriau sukurti naują kitoje klasėje.

"Roslyn" kompiliatorius rūpinasi kodo struktūrizavimu, todėl čia mes ketiname išanalizuoti struktūrizuotus duomenis ir parašyti kodą.

Pirmiausia paskelbsiu visą kodą. Stengiausi turėti kuo mažiau kodo. Šį kartą tai veikia, tačiau tai tik tokiame lygyje, kuris juda kaip ponas/ponia, taigi, kai iš tikrųjų tęsite plėtrą, bus nemažai trūkstamų dalių. Prašome ten tobulėti, jei reikia.

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

Apie tai rašiau komentaruose, bet kai kuriose vietose tai paaiškinsiu.

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

context.SyntaxProvider.CreateSyntaxProvider Išanalizuoti kodą programos projekte ir kiek įmanoma jį struktūrizuoti. Viskas yra suskirstyta į mazgus, ir mes naudojame norėdami nustatyti, kuriuos iš jų norime predicate apdoroti. Taip pat, jei reikia transform , konvertuokite apdorotą objektą ir perduokite jį į išvesties pusę.

predicate Šiuo atveju mes darome šiuos veiksmus.

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

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

Visų pirma, kaip ir bet kurio proceso metu, automatinio kodo generavimo procesas visada gali būti atšauktas per anksti. Taigi naudokite atšaukimo žetoną, kad paskambintumėte ThrowIfCancellationRequested , kad galėtumėte bet kada pertraukti.

predicate Dabar, kai kiekvienas mazgas yra iškviestas, norime nustatyti, kurį iš jų norime apdoroti. Kadangi jų yra labai daug, geriau juos tam tikru mastu susiaurinti.

Kadangi šį kartą į klasę įtrauksime apdorojimą, nustatysime ClassDeclarationSyntax , ar taip yra, ir nustatysime, ar bus apdorojama tik klasė. partial class Be to, kadangi kodas yra pridėtas , jis partial class pateikiamas kaip sprendimas.

// 対象のノードをコード出力に必要な形に変換します
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 leidžia konvertuoti analizę į reikiamą formą ir perduoti ją kodo išvestims. Šį kartą mes nedarome daug konversijų, bet perduodame vertę išvesties pusei tokią, kokia ji yra return . Pakeliui gauname "deklaruotus simbolius", tačiauSemanticModel galime gauti daug papildomos informacijos naudodami ir Syntac , todėl manau, kad prireikus galime ją gauti. return Rinkiniai sukuriami ir grąžinami, tačiau duomenų, kuriuos reikia perduoti į išvesties pusę, konfigūracija gali būti bet kokia. Jei norite perduoti kelis duomenis, galite sukurti tokį rinkinį arba galite apibrėžti savo klasę ir perduoti 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 Dabar sugeneruosime ir išvesime kodą pagal duomenis, kuriuos turi apdoroti . context.SyntaxProvider.CreateSyntaxProvider return Reikšmę galima gauti kaip antrąjį argumentą Action , todėl kodas generuojamas pagal tą vertę.

Šiuo atveju mes kuriame kodą, kad gautume sintaksės ypatybių sąrašą iš klasės sintaksės ir nustatytume default ypatybę iš kiekvieno pavadinimo.

Po to sukurkite metodą pagal Reset klasės pavadinimą, įdėkite anksčiau sukurto ypatybių sąrašo kodą ir nustatykite visų ypatybių reikšmę default atgal Reset į Metodas baigtas.

Kodas išvedamas contextSource.AddSource atskirai kiekvienai metodo klasei. Beje, priežastis, kodėl į failo pavadinimą įdėjau "g", yra ta, kad būtų lengviau nustatyti, ar tai rankiniu būdu sukurtas kodas, ar automatiškai sugeneruota kodo klaida, kai yra kūrimo klaida.

Jei iš tikrųjų tai padarysite, gausite tokias užklausas kaip "Noriu iš naujo nustatyti lauką", "Noriu jį inicijuoti kitaip nei numatytasis", "Noriu iš naujo nustatyti tik automatines ypatybes". Jei juos įdėsite, laidas bus ilgas, todėl pabandykite jį pasigaminti patys.

Paraiškos projekto pusė

Šį kartą sukūriau kodą, kuris praplečia klasės metodą, todėl tinkamai sukursiu klasę.

Turinys yra toks, bet jei yra savybių, likusią dalį galite naudoti atitinkamai.

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

Metodas išplečiamas, kai Reset kuriate kodą, tačiau jis gali neatsispindėti "Visual Studio", todėl tuo metu iš naujo paleiskite "Visual Studio". Manau, matote, kad kodas automatiškai išplečiamas į analizatorių.

Įtrauka yra keista, tačiau galite jos nepaisyti, nes iš esmės neliečiate automatiškai sugeneruoto kodo.

Program.cs Pabandykite parašyti kodą, kad sužinotumėteReset, ar galite paskambinti šiuo metodu. Manau, kad vykdymo rezultatai atsispindi taip, kaip tikėtasi.

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