Gunakan Penjana Visual Studio dan Sumber untuk menjana kod secara automatik

Laman dikemaskini :
Tarikh penciptaan halaman :

Persekitaran operasi

Visual Studio
  • Visual Studio 2022
.BERSIH
  • .NET 8.0

Prasyarat

Visual Studio
  • Ia berfungsi walaupun dengan versi yang agak lama
.BERSIH
  • Ia berfungsi walaupun dengan versi yang agak lama

Pada mulanya

Terdapat beberapa teknik untuk menjana kod secara automatik dengan definisi anda sendiri, tetapi dalam artikel ini saya akan menunjukkan kepada anda cara menggunakan Penjana Sumber. Salah satu kelebihan terbesar penjana sumber ialah ia menganalisis struktur kod sumber projek semasa dan menghasilkan kod baru berdasarkannya. Sebagai contoh, apabila anda membuat kelas baru, anda boleh membuatnya supaya kod ditambahkan secara automatik agar sesuai dengan kelas. Anda boleh memprogram jenis kod yang ingin anda hasilkan, jadi anda boleh membuat sebarang bentuk penjanaan kod automatik yang anda suka.

Kod ini pada dasarnya dihasilkan secara automatik di latar belakang dan dimasukkan ke dalam projek di belakang tabir. Oleh kerana ia bukan output sebagai fail dengan cara yang boleh dilihat, ia tidak digunakan untuk tujuan menggunakan semula kod yang dihasilkan secara automatik untuk tujuan umum (walaupun ia boleh dikeluarkan dengan menyalinnya buat masa ini). Walau bagaimanapun, kerana kod dijana secara automatik mengikut struktur projek, ia mengurangkan kos input manual dan mengurangkan kesilapan menulis kod dengan sewajarnya, yang merupakan kelebihan yang besar.

Dalam artikel ini, saya akan menerangkan cara memeriksa bahawa kod dijana secara automatik, jadi saya tidak akan pergi sejauh untuk benar-benar menganalisis kod secara mendalam dan melakukan output lanjutan. Sila lihat sendiri sebagai aplikasi.

Persediaan

Mula-mula, pasang Visual Studio. Penjelasan ringkas diringkaskan dalam Petua berikut.

Pada asasnya, anda boleh menggunakannya untuk sebarang projek, jadi tidak kira beban kerja yang anda sediakan. Walau bagaimanapun, kali ini, sebagai "komponen individu", ". Platform Pengkompil NET SDK. Ini berguna untuk penyahpepijatan semasa pembangunan Penjana Sumber. Jika anda telah memasang Visual Studio, anda boleh menambahnya daripada menu Visual Studio di bawah Alat > Dapatkan Alat dan Ciri.

Membuat dan Menyediakan Projek Penjana Sumber

Penjana Sumber dicipta dalam projek yang berasingan daripada projek aplikasi utama. Tidak kira sama ada anda menciptanya terlebih dahulu atau membuat yang tambahan kemudian. Dalam kes ini, saya akan menciptanya dari projek Penjana Sumber.

Pada skrin Cipta Projek Baru, pilih Pustaka Kelas.

Nama projek boleh menjadi apa-apa, tetapi buat masa ini CodeGenerator , kami akan meninggalkannya sebagai .

Untuk perpustakaan kelas, Penjana Sumber kini menyokong . Piawaian BERSIH 2.0.

Sebaik sahaja anda membuat projek anda, dapatkan pakej dengan Microsoft.CodeAnalysis.CSharp NuGet. Tingkah laku mungkin berbeza bergantung pada versi, tetapi tidak ada gunanya terus menggunakan versi lama, jadi saya akan meletakkan versi terkini.

Kemudian buka fail projek sebagai kod.

Apabila anda membukanya, anda akan melihat perkara berikut.

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

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

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

</Project>

Tambahkannya seperti berikut: Jangan ragu untuk menambah apa-apa lagi yang anda perlukan.

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

Seterusnya, tulis kod untuk kelas yang akan menghasilkan kod secara automatik. Pertama sekali, kami hanya akan membuat rangka kerja, jadi sila tulis semula kod sedia ada dari awal Class1.cs atau tambah kod baru.

Kod harus kelihatan seperti ini: Nama kelas boleh menjadi apa-apa, tetapi lebih baik mempunyai nama yang menunjukkan jenis kod yang dihasilkan secara automatik. SampleGenerator Buat masa ini, biarkan ia sebagai . Initialize Dalam kaedah ini, anda akan menulis analisis kod dan proses penjanaan kod.

using Microsoft.CodeAnalysis;

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

Seterusnya, buat projek untuk aplikasi yang anda mahu menjana kod secara automatik. Ia boleh menjadi apa-apa, tetapi di sini kita hanya akan menggunakannya sebagai aplikasi konsol. Jenis dan versi rangka kerja projek yang mana kod dijana secara automatik sepadan dengan nombor umum.

Tambah rujukan kepada projek penjana sumber dari projek sisi aplikasi.

Sesetengah tetapan tidak boleh disetkan dalam sifat, jadi buka fail projek dalam kod.

Saya fikir ia kelihatan seperti ini:

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

Tambah seting untuk projek yang dirujuk seperti berikut: Pastikan anda tidak membuat kesilapan dalam sintaks 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>

Seterusnya, buka sifat projek di "Penjana Kod sisi".

Klik pautan untuk Membuka UI Hartanah Pelancaran Debug.

Padamkan profil asal kerana anda tidak mahu menggunakannya.

Tambah profil baharu.

Pilih Komponen Roslyn.

Jika anda telah membuat tetapan setakat ini, anda sepatutnya boleh memilih projek aplikasi, jadi pilihnya.

Ini adalah akhir penyediaan.

Semak sama ada anda boleh menyahpepijat

Buka kod penjana sumber dan Initialize letakkan titik putus pada akhir kaedah.

Mari nyahpepijat penjana sumber.

Jika proses berhenti di titik putus, anda boleh mengesahkan bahawa anda menyahpepijat secara normal. Ini semestinya memudahkan pembangunan penjana sumber anda.

Buat masa ini, mari kita output kod tetap

Mula-mula, mari kita cuba mengeluarkan kod tetap dengan mudah. Ia mudah kerana anda tidak perlu menganalisis kod tersebut. Walaupun ia adalah kod tetap, ia dikendalikan sebagai rentetan, jadi adalah mungkin untuk meningkatkan pengeluaran kod dalam bentuk tetap dengan membina program.

Jika anda ingin mengeluarkan kod tetap, anda context.RegisterPostInitializationOutput boleh berbuat demikian dengan menggunakan . Berikut adalah contoh output kod.

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

Kandungan kod adalah seperti yang ditulis dalam komen, jadi saya akan meninggalkan butirannya. Bina dan pastikan tiada kesilapan.

Apabila binaan selesai, anda boleh mengembangkan "Analyzers" dalam projek aplikasi untuk melihat kod yang dihasilkan oleh penjana kod.

Jika anda tidak melihatnya, mulakan semula Visual Studio dan semaknya. Nampaknya pada masa ini, Visual Studio mungkin tidak dikemas kini melainkan anda memulakannya semula. Intellisence dan sintaksis yang menyerlahkan yang digunakan semasa membuat program adalah serupa. Walau bagaimanapun, kerana kod itu sendiri dapat dilihat pada masa binaan, nampaknya program ini dapat dilihat di sisi aplikasi.

Setelah kod dijana, cuba gunakannya dalam aplikasi anda. Ia sepatutnya berfungsi dengan baik.

Menganalisis dan menjana kod

Sekiranya anda hanya memproses kod dengan normal dan mengeluarkannya, ia tidak jauh berbeza dengan penjanaan kod automatik yang lain, dan anda tidak boleh memanfaatkan faedah penjana sumber. Jadi sekarang mari kita analisa kod projek aplikasi dan hasilkan kod dengan sewajarnya.

Walau bagaimanapun, analisis kod itu agak mendalam, jadi saya tidak dapat menjelaskan segala-galanya di sini. Buat masa ini, saya akan menerangkan sehingga ke titik di mana anda boleh menganalisis dan mengeluarkan kod tersebut. Jika anda ingin pergi lebih mendalam, sila lakukan penyelidikan anda sendiri.

Buat masa ini, sebagai contoh, saya ingin membuat kod secara automatik "tambah kaedah ke Reset semua kelas yang dibuat dan tetapkan semula nilai default semua sifat kepada ". Dalam tahun-tahun kebelakangan ini, nampaknya pengendalian kejadian yang tidak berubah telah diutamakan, tetapi dalam program permainan, tidak mungkin untuk membuat contoh baru setiap bingkai, jadi saya fikir ada penggunaan untuk proses menetapkan semula nilai. default Anda mungkin mahu menetapkan sesuatu selain daripada itu, tetapi jika anda berbuat demikian, ia akan menjadi contoh pertama yang panjang, jadi sila gunakannya sendiri.

Bahagian Penjana Kod

Buat kelas baru untuk tujuan mengekalkan penjana yang anda buat sebelum ini. Sekiranya kandungan dijana secara automatik berubah, lebih baik membuat yang baru di kelas lain.

Pengkompil Roslyn menjaga penstrukturan kod, jadi apa yang akan kita lakukan di sini adalah menghuraikan data berstruktur dan menulis kod tersebut.

Pertama, saya akan menyiarkan semua kod. Saya telah cuba untuk mempunyai kod sesedikit mungkin. Kali ini ia berfungsi, tetapi hanya pada tahap yang bergerak sebagai Mr./Ms., jadi apabila anda benar-benar meneruskan pembangunan, akan ada beberapa bahagian yang hilang. Sila perbaiki di sana mengikut keperluan.

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

Saya telah menulis mengenainya dalam komen, tetapi saya akan menerangkannya di beberapa tempat.

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

context.SyntaxProvider.CreateSyntaxProvider untuk menganalisis kod dalam projek aplikasi dan strukturnya sebanyak mungkin. Segala-galanya dibahagikan kepada nod, dan kami gunakan untuk menentukan mana antara mereka yang ingin kami predicate proses. Juga, jika perlu transform , tukar objek yang diproses dengan dan lulus ke bahagian output.

predicate Dalam kes ini, kita melakukan perkara berikut.

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

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

Pertama sekali, seperti mana-mana proses, proses menjana kod secara automatik sentiasa boleh dibatalkan lebih awal. Oleh itu, gunakan token pembatalan untuk ThrowIfCancellationRequested memanggil supaya anda boleh mengganggu pada bila-bila masa.

predicate Sekarang setiap nod dipanggil, kita mahu menentukan mana yang kita mahu proses. Oleh kerana terdapat sejumlah besar daripada mereka, lebih baik menyempitkannya di sini sedikit sebanyak.

Oleh kerana kami akan menambah pemprosesan ke kelas kali ini, kami ClassDeclarationSyntax akan menentukan sama ada ia adalah dan menentukan sama ada hanya kelas yang akan diproses. partial class Juga, kerana kod itu dilampirkan , ia partial class diletakkan sebagai penghakiman.

// 対象のノードをコード出力に必要な形に変換します
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 membolehkan anda menukar analisis ke dalam bentuk yang diperlukan dan lulus ke output kod. Kali ini, kami tidak melakukan banyak penukaran, tetapi lulus nilai ke sisi output kerana ia adalah return . Kami mendapat "simbol yang diisytiharkan" di sepanjang jalan, tetapiSemanticModel kita boleh mendapatkan banyak maklumat tambahan menggunakan dan Syntac , jadi saya fikir kita boleh mendapatkannya jika perlu. return Tuples dicipta dan dikembalikan, tetapi konfigurasi data yang akan dihantar ke bahagian output boleh menjadi apa-apa. Jika anda ingin lulus berbilang data, anda boleh membuat tuple seperti ini, atau anda boleh menentukan kelas anda sendiri dan lulus.

// 解析し変換した情報をもとにコードを出力します
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 Sekarang, kami akan menjana dan mengeluarkan kod berdasarkan data yang akan diproses oleh . context.SyntaxProvider.CreateSyntaxProvider return Nilai boleh diterima sebagai argumen Action kedua , jadi kod dijana berdasarkan nilai tersebut.

Dalam kes ini, kami mencipta kod untuk mendapatkan senarai sintaks sifat dari sintaks kelas dan menetapkan sifat dari default setiap nama.

Selepas itu, buat kaedah berdasarkan Reset nama kelas, masukkan kod senarai harta yang dibuat sebelum ini, dan tetapkan nilai default semua sifat kembali Reset ke Kaedah selesai.

Kod ini dikeluarkan contextSource.AddSource secara berasingan untuk setiap kelas dalam kaedah tersebut. Dengan cara ini, sebab mengapa saya meletakkan "g" dalam nama fail adalah untuk memudahkan menentukan sama ada ia dibuat kod secara manual atau ralat kod yang dihasilkan secara automatik apabila terdapat ralat binaan.

Jika anda benar-benar membuatnya, anda akan menerima permintaan seperti "Saya mahu menetapkan semula medan", "Saya mahu memulakannya selain daripada lalai", "Saya mahu menetapkan semula hanya sifat automatik". Jika anda memasukkannya, tali pusat akan panjang, jadi cuba buat sendiri.

Bahagian projek aplikasi

Kali ini, saya mencipta kod yang memanjangkan kaedah kelas, jadi saya akan membuat kelas dengan sewajarnya.

Kandungannya adalah seperti berikut, tetapi jika terdapat sifat, anda boleh menggunakan selebihnya mengikut kesesuaian.

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

Kaedah ini dilanjutkan apabila Reset anda membuat kod, tetapi ia mungkin tidak dapat dilihat dalam Visual Studio, jadi mulakan semula Visual Studio pada masa itu. Saya fikir anda dapat melihat bahawa kod itu secara automatik diperluaskan kepada penganalisis.

Pengengsotan itu pelik, tetapi anda boleh mengabaikannya kerana pada dasarnya anda tidak menyentuh kod yang dihasilkan secara automatik.

Program.cs Cuba menulis kod untuk melihat Reset sama ada anda boleh memanggil kaedah tersebut. Saya rasa keputusan pelaksanaan ditunjukkan seperti yang diharapkan.

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