Menggunakan Visual Studio dan Source Generator untuk menghasilkan kode secara otomatis

Halaman Diperbarui :
Tanggal pembuatan halaman :

Lingkungan operasi

Visual Studio
  • Visual Studio 2022
.JARING
  • .NET 8.0

Prasyarat

Visual Studio
  • Ia bekerja bahkan dengan versi yang agak lama
.JARING
  • Ia bekerja bahkan dengan versi yang agak lama

Pada awalnya

Ada beberapa teknik untuk menghasilkan kode secara otomatis dengan definisi Anda sendiri, tetapi dalam artikel ini saya akan menunjukkan cara menggunakan Source Generator. Salah satu keuntungan terbesar dari generator sumber adalah menganalisis struktur kode sumber proyek saat ini dan menghasilkan kode baru berdasarkan itu. Misalnya, saat membuat kelas baru, Anda dapat membuatnya sehingga kode ditambahkan secara otomatis agar sesuai dengan kelas. Anda dapat memprogram jenis kode apa yang ingin Anda hasilkan, sehingga Anda dapat membuat segala bentuk pembuatan kode otomatis yang Anda suka.

Kode ini pada dasarnya dihasilkan secara otomatis di latar belakang dan dimasukkan ke dalam proyek di belakang layar. Karena tidak output sebagai file dengan cara yang terlihat, itu tidak digunakan untuk tujuan menggunakan kembali kode yang dihasilkan secara otomatis untuk tujuan umum (meskipun dapat dihapus dengan menyalinnya untuk saat ini). Namun, karena kode secara otomatis dihasilkan sesuai dengan struktur proyek, ini mengurangi biaya input manual dan mengurangi kesalahan penulisan kode yang sesuai, yang merupakan keuntungan besar.

Pada artikel ini, saya akan menjelaskan cara memeriksa apakah kode tersebut dihasilkan secara otomatis, jadi saya tidak akan melangkah lebih jauh untuk benar-benar menganalisis kode secara mendalam dan melakukan output lanjutan. Silakan cari sendiri sebagai aplikasi.

Setup

Pertama, instal Visual Studio. Penjelasan singkatnya terangkum dalam Tips berikut ini.

Pada dasarnya, Anda dapat menggunakannya untuk proyek apa pun, jadi tidak masalah beban kerja mana yang Anda atur. Namun, kali ini, sebagai "komponen individu", ". SDK Platform Kompiler NET. Ini berguna untuk debugging selama pengembangan Source Generator. Jika Anda sudah menginstal Visual Studio, Anda dapat menambahkannya dari menu Visual Studio di bawah Alat > Dapatkan Alat dan Fitur.

Membuat dan Mempersiapkan Proyek Generator Sumber

Source Generator dibuat dalam proyek yang terpisah dari proyek aplikasi utama. Tidak masalah jika Anda membuatnya terlebih dahulu atau membuat yang tambahan nanti. Dalam hal ini, saya akan membuatnya dari proyek Source Generator.

Di layar Buat Proyek Baru, pilih Perpustakaan Kelas.

Nama proyek bisa apa saja, tetapi untuk saat ini CodeGenerator , kami akan membiarkannya sebagai .

Untuk pustaka kelas, Source Generator saat ini mendukung . Standar NET 2.0.

Setelah Anda membuat proyek Anda, dapatkan paket dengan Microsoft.CodeAnalysis.CSharp NuGet. Perilaku mungkin berbeda tergantung pada versi, tetapi tidak ada gunanya terus menggunakan versi lama, jadi saya akan meletakkan versi terbaru.

Kemudian buka file proyek sebagai kode.

Saat Anda membukanya, Anda akan melihat yang berikut ini.

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

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

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

</Project>

Tambahkan sebagai berikut: Jangan ragu untuk menambahkan hal lain yang Anda butuhkan.

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

Selanjutnya, tulis kode untuk kelas yang secara otomatis akan menghasilkan kode. Pertama-tama, kita hanya akan membuat kerangka kerja, jadi silakan tulis ulang kode yang ada dari awal Class1.cs atau tambahkan kode baru.

Kode akan terlihat seperti ini: Nama kelas bisa apa saja, tetapi lebih baik memiliki nama yang menunjukkan jenis kode apa yang dihasilkan secara otomatis. SampleGenerator Untuk saat ini, biarkan sebagai . Initialize Dalam metode ini, Anda akan menulis analisis kode dan proses pembuatan kode.

using Microsoft.CodeAnalysis;

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

Selanjutnya, buat proyek untuk aplikasi yang ingin Anda buat kodenya secara otomatis. Itu bisa apa saja, tetapi di sini kita hanya akan menggunakannya sebagai aplikasi konsol. Jenis dan versi kerangka kerja proyek tempat kode dihasilkan secara otomatis sesuai dengan nomor umum.

Tambahkan referensi ke proyek generator sumber dari proyek sisi aplikasi.

Beberapa pengaturan tidak dapat diatur dalam properti, jadi buka file proyek dalam kode.

Saya pikir tampilannya 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>

Tambahkan pengaturan untuk proyek yang direferensikan sebagai berikut: Pastikan bahwa Anda tidak membuat kesalahan 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>

Selanjutnya, buka properti proyek di "sisi Code Generator".

Klik tautan untuk Membuka UI Properti Peluncuran Debug.

Hapus profil asli karena Anda tidak ingin menggunakannya.

Tambahkan profil baru.

Pilih Komponen Roslyn.

Jika Anda telah membuat pengaturan sejauh ini, Anda harus dapat memilih proyek aplikasi, jadi pilihlah.

Ini adalah akhir dari persiapan.

Periksa apakah Anda dapat men-debug

Buka kode generator sumber dan Initialize tempatkan breakpoint di akhir metode.

Mari kita debug generator sumber.

Jika proses berhenti di breakpoint, Anda dapat mengonfirmasi bahwa Anda melakukan debug secara normal. Ini akan membuat pengembangan generator sumber Anda cukup mudah.

Untuk saat ini, mari kita keluarkan kode tetap

Pertama, mari kita coba menampilkan kode tetap dengan mudah. Sangat mudah karena Anda bahkan tidak perlu menganalisis kode. Bahkan jika itu adalah kode tetap, itu ditangani sebagai string, sehingga dimungkinkan untuk meningkatkan produksi kode dalam bentuk tetap dengan membangun sebuah program.

Jika Anda ingin menampilkan kode tetap, Anda context.RegisterPostInitializationOutput dapat melakukannya dengan menggunakan . Berikut ini adalah contoh output 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));
    });
  }
}

Isi kodenya seperti yang tertulis di komentar, jadi saya akan menghilangkan detailnya. Bangun dan pastikan tidak ada kesalahan.

Ketika build selesai, Anda dapat memperluas "Analyzers" dalam proyek aplikasi untuk melihat kode yang dihasilkan oleh generator kode.

Jika Anda tidak melihatnya, mulai ulang Visual Studio dan periksa. Tampaknya saat ini, Visual Studio mungkin tidak diperbarui kecuali Anda me-restart itu. Intellisence dan syntax highlighting yang digunakan saat membuat program serupa. Namun, karena kode itu sendiri tercermin pada saat pembuatan, tampaknya program tercermin di sisi aplikasi.

Setelah kode dibuat, coba gunakan di aplikasi Anda. Ini harus bekerja dengan baik.

Menganalisis dan menghasilkan kode

Jika Anda hanya memproses kode secara normal dan mengeluarkannya, itu tidak jauh berbeda dari pembuatan kode otomatis lainnya, dan Anda tidak dapat memanfaatkan manfaat dari generator sumber. Jadi sekarang mari kita menganalisis kode proyek aplikasi dan menghasilkan kode yang sesuai.

Namun, analisis kodenya cukup dalam, jadi saya tidak bisa menjelaskan semuanya di sini. Untuk saat ini, saya akan menjelaskan sampai pada titik di mana Anda dapat menganalisis dan mengeluarkan kode. Jika Anda ingin masuk lebih dalam, silakan lakukan riset sendiri.

Untuk saat ini, sebagai contoh, saya ingin secara otomatis membuat kode "tambahkan metode ke Reset semua kelas yang dibuat dan atur ulang nilai default semua properti ke ". Dalam beberapa tahun terakhir, tampaknya menangani instance yang tidak dapat diubah lebih disukai, tetapi dalam program game, tidak mungkin untuk membuat instance baru setiap frame, jadi saya pikir ada gunanya untuk proses mengatur ulang nilai. default Anda mungkin ingin menetapkan sesuatu selain itu, tetapi jika Anda melakukannya, itu akan menjadi contoh pertama yang panjang, jadi silakan terapkan sendiri.

Sisi Pembuat Kode

Buat kelas baru untuk tujuan menjaga generator yang Anda buat sebelumnya. Jika konten yang akan dihasilkan secara otomatis berubah, lebih baik membuat yang baru di kelas lain.

Kompiler Roslyn menangani penataan kode, jadi yang akan kita lakukan di sini adalah mengurai data terstruktur dan menulis kode.

Pertama, saya akan memposting semua kode. Saya sudah mencoba memiliki kode sesedikit mungkin. Kali ini berhasil, tetapi hanya pada level yang bergerak sebagai Mr./Ms., jadi ketika Anda benar-benar melanjutkan pengembangan, akan ada beberapa bagian yang hilang. Harap tingkatkan di sana sesuai kebutuhan.

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 sudah menulisnya di komentar, tapi saya akan menjelaskannya di beberapa tempat.

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

context.SyntaxProvider.CreateSyntaxProvider untuk menganalisis kode dalam proyek aplikasi dan menyusunnya sebanyak mungkin. Semuanya dibagi menjadi beberapa node, dan kami gunakan untuk menentukan mana yang ingin kami predicate proses. Juga, jika perlu transform , ubah objek yang diproses dengan dan berikan ke sisi output.

predicate Dalam hal ini, kami melakukan hal 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-tama, seperti halnya proses apa pun, proses pembuatan kode secara otomatis selalu dapat dibatalkan sebelum waktunya. Jadi gunakan token pembatalan untuk ThrowIfCancellationRequested menelepon sehingga Anda dapat mengganggu kapan saja.

predicate Sekarang setiap node dipanggil, kami ingin menentukan mana yang ingin kami proses. Karena jumlahnya sangat banyak, lebih baik mempersempitnya di sini sampai batas tertentu.

Karena kita akan menambahkan pemrosesan ke kelas kali ini, kita ClassDeclarationSyntax akan menentukan apakah itu dan menentukan apakah hanya kelas yang akan diproses. partial class Juga, karena kode dilampirkan dengan , itu partial class dimasukkan sebagai penilaian.

// 対象のノードをコード出力に必要な形に変換します
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 memungkinkan Anda untuk mengubah analisis menjadi formulir yang diperlukan dan meneruskannya ke output kode. Kali ini, kami tidak melakukan banyak konversi, tetapi meneruskan nilainya ke sisi output apa adanya return . Kami mendapatkan "simbol yang dinyatakan" di sepanjang jalan, tetapiSemanticModel kami bisa mendapatkan banyak informasi tambahan menggunakan dan Syntac , jadi saya pikir kami bisa mendapatkannya jika perlu. return Tupel dibuat dan dikembalikan, tetapi konfigurasi data yang akan diteruskan ke sisi output bisa apa saja. Jika Anda ingin meneruskan beberapa data, Anda dapat membuat tupel seperti ini, atau Anda dapat menentukan kelas Anda sendiri dan meneruskannya.

// 解析し変換した情報をもとにコードを出力します
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, kita akan menghasilkan dan mengeluarkan kode berdasarkan data yang akan diproses oleh . context.SyntaxProvider.CreateSyntaxProvider return Nilai of dapat diterima sebagai argumen kedua dari Action , sehingga kode dihasilkan berdasarkan nilai tersebut.

Dalam hal ini, kita membuat kode untuk mendapatkan daftar sintaks properti dari sintaks kelas dan mengatur default properti dari setiap nama.

Setelah itu, buat metode berdasarkan Reset nama kelas, sematkan kode daftar properti yang dibuat sebelumnya, dan atur nilai default semua properti kembali Reset ke Metode selesai.

Kode adalah contextSource.AddSource output secara terpisah untuk setiap kelas dalam metode. Omong-omong, alasan mengapa saya memasukkan "g" dalam nama file adalah untuk membuatnya lebih mudah untuk menentukan apakah itu kode yang dibuat secara manual atau kesalahan kode yang dibuat secara otomatis ketika ada kesalahan build.

Jika Anda benar-benar membuatnya, Anda akan menerima permintaan seperti "Saya ingin mengatur ulang bidang", "Saya ingin menginisialisasi selain default", "Saya hanya ingin mengatur ulang properti otomatis". Jika Anda memasukkannya, kabelnya akan panjang, jadi cobalah membuatnya sendiri.

Sisi proyek aplikasi

Kali ini, saya membuat kode yang memperluas metode kelas, jadi saya akan membuat kelas dengan tepat.

Isinya adalah sebagai berikut, tetapi jika ada properti, Anda dapat menggunakan sisanya sebagaimana mestinya.

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

Metode ini diperpanjang saat Reset Anda membuat kode, tetapi mungkin tidak tercermin dalam Visual Studio, jadi mulai ulang Visual Studio pada saat itu. Saya pikir Anda dapat melihat bahwa kode secara otomatis diperluas ke penganalisis.

Lekukannya aneh, tetapi Anda dapat mengabaikannya karena pada dasarnya Anda tidak menyentuh kode yang dibuat secara otomatis.

Program.cs Coba tulis kode untuk melihat Reset apakah Anda dapat memanggil metode ini. Saya pikir hasil eksekusi tercermin 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();