CodeGym /コース /C# SELF /はじめに Source Generators

はじめに Source Generators

C# SELF
レベル 63 , レッスン 3
使用可能

1. はじめに

コードを書いていると、ふと「なぜ同じコードのテンプレートを別々のクラスで何度もコピーしているんだろう?」とか「シリアライゼーション、ロギング、データマッピングがこんなに同じような行だらけなのはなぜだ?」と感じることがあるはずです。そんなとき、誰か(あるいは何か)にその面倒なテンプレートを書いてほしくなります。

そこで登場するのがSource Generatorsです。これは .NET 5 で導入され、今も進化している C# の機能です。Source Generator はコンパイル時に実行され、プロジェクトに自動的に組み込まれる C# コードを動的に生成できるライブラリです。

なぜ必要なのか?

  • ルーチンの自動化: 同じようなクラスやメソッド(boilerplate)を書く手間を省けます。
  • コンパイラで検証できる安全性: 生成されたコードはあなたのコードと一緒にコンパイルされます(T4 やリフレクションとは異なります)。
  • 高いパフォーマンス: シリアライゼーション、DI、マッピングなどをランタイムのリフレクション無しで行えます。
  • モダンなパターンのサポート: コード生成がなければ実現が難しかったパターンを扱えます。

Source Generatorsは「内部で」どう動くのか?

Source Generator は .NET ライブラリ(通常は Class Library タイプのプロジェクト)で、ISourceGenerator インターフェイスを実装します。コンパイル時にRoslynがすべての登録されたジェネレータを起動し、プロジェクトの構文ツリーへのアクセスを提供します。

ジェネレータはコードを解析して、何をどこで生成するかを決め、新しい C# ファイルを作成し、それらはコンパイラによってすぐにコンパイルされます。

自動的な ToString の生成

まずは簡単な例から。プロパティがたくさんあるクラスがあって、ToString を実装する必要があるとします。手書きだとこんな感じになります:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
        => $"Person(Name={Name}, Age={Age})";
}

しかしプロパティが増えると手間がかかり、更新を忘れがちです。Source Generatorがそれを代わりにやってくれます。

2. 自分の Source Generator を作るには?

プロジェクトの作成

JetBrains Rider や Visual Studio を開き、Class Library (.NET Standard) タイプの新しいプロジェクトを作成します — こうしたプロジェクトがジェネレータになり得ます。次に NuGet パッケージを追加します:

  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.Analyzers

重要な属性

  • [Generator] — このクラスが Source Generator であることを示します。

最小限のジェネレータのテンプレート

こちらが動作する最小の例です:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // 追加のアクションを登録できる(任意)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var code = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Privet, mir! Ya sgenerirovan!"";
    }
}";
        context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
    }
}

このシンプルなジェネレータはコンパイル時に常に静的クラス HelloWorld とメソッド SayHello を追加します。

メインアプリで Source Generators を使うには?

ジェネレータプロジェクトを NuGet パッケージとして、またはProject Referenceとして、Analyzer セクションに追加します(詳細は 公式ドキュメント を参照)。

生成されたコードはすぐにプロジェクトで使えます — 追加で何かを参照する必要はなく、ただ使うだけです:

// これは自動生成されます!
using Generated;

Console.WriteLine(HelloWorld.SayHello());

3. 実例:ToString の自動生成

属性 [AutoToString] が付いたすべてのクラスに自動的に ToString 実装を追加したいとします。そのために以下が必要です:

  • 自分の属性を作ること。
  • その属性が付いたすべてのクラスを解析すること。
  • 各クラスに対して ToString メソッドを生成すること。

属性

[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}

コードでの使用例

[AutoToString]
public class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}

単純な生成ロジック

ジェネレータは [AutoToString] の付いたクラスを探して、だいたいこんなコードを生成します:

public override string ToString() 
    => $"Product(Name={Name}, Price={Price})";

ジェネレータの実際のコード片

基本的なアイデアは Roslyn で構文ツリーを巡回することです:

public void Execute(GeneratorExecutionContext context)
{
    // すべての構文ツリーを解析する
    foreach (var tree in context.Compilation.SyntaxTrees)
    {
        var root = tree.GetRoot();
        // 必要な属性を持つクラスを探す(概念例)
        var classes = root.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .Where(c => c.AttributeLists
                         .SelectMany(al => al.Attributes)
                         .Any(a => a.Name.ToString().Contains("AutoToString")));

        foreach (var @class in classes)
        {
            var className = @class.Identifier.Text;
            // クラスのすべてのプロパティを取得する
            var props = @class.Members
                .OfType<PropertyDeclarationSyntax>()
                .Select(p => p.Identifier.Text)
                .ToArray();

            var toStringCode = string.Join(", ", props.Select(p => $"{p}={{this.{p}}}"));
            var generated = $@"
partial class {className}
{{
    public override string ToString() => $""{className}({toStringCode})"";
}}";

            context.AddSource($"{className}_ToString", SourceText.From(generated, Encoding.UTF8));
        }
    }
}

注意:production 用のコードでは、Roslyn の SemanticModel を使ったより正確な解析を使うのが一般的です。

4. 役に立つニュアンス

注意すべき点

Source Generatorsは既存のソースを変更することはできず、新しいファイルだけを作成できます(例えば追加の partial クラスやメソッドなど)。つまり、クラスが partial として宣言されていれば、そこに追加のメソッドやプロパティを生成できます。

構文を正しく解析して言語のすべてのニュアンス(ネストしたクラス、ジェネリクス、修飾子など)に対応するのは難しいことがあります。ジェネレータ作者は生成コードがコンパイル可能でプロジェクトを壊さないよう注意する必要があります。

もう一つの落とし穴は、インターフェイスを実装するメソッドを生成する場合、生成ファイルが毎回ビルド時に作られることを確認する必要がある点です。さもないと奇妙なコンパイルエラーが起きることがあります。最近のツールはこれらの問題を解決しますが、覚えておくべきポイントです。

Source Generators とリフレクションの比較

リフレクション: ランタイムで実行され、コストが高く、コンパイラで検証されないことがあり、大量データでは遅くなることが多いです。

Source Generator: コンパイル時にコードを生成します。すべて静的にチェックされ、IDE はメソッドを認識し、補完が機能し、パフォーマンスは通常の C# コードと同等です。

実用的な利点

  • System.Text.Json: リフレクションなしでシリアライゼーション/デシリアライゼーションを生成する。
  • DI コンテナの設計: 例えば Microsoft.Extensions.DependencyInjection で依存グラフを生成する。
  • Mapster のようなマッパー: リフレクションからコンパイル時生成の mapping コードへ移行する。
  • テストフレームワーク: 属性に基づくテストメソッドの自動生成。
  • ASP.NET Minimal APIs (.NET 7 以降): endpoint ハンドラの生成。

設定、パラメータ、オプション

ジェネレータは MSBuild パラメータ、追加ファイル、コンベンションを通して設定できます。例えば環境(Debug/Release)やアプリ構成に応じて異なる ToString を生成することも可能です。

Source Generators が実際の問題にどう結びつくか

クリーンで高速なコードを目指す開発者にとって非常に有用なツールです:ルーチンが減り、compile-time チェックが増え、IDE の補助やリファクタリングが分かりやすくなります。ジェネレータの知識はシリアライゼーションや DI、マッピングなどの分野で面接でも問われることが増えています。

Source Generator のライフサイクル

段階 何が起こるか
1. プロジェクトが追加される ジェネレータが analyzer/reference として追加される
2. Roslyn がソースをコンパイルする ジェネレータは AST(抽象構文木)を受け取る
3. ジェネレータが実行される コンパイル用の新しい .cs ファイルを追加する
4. すべてがコンパイルされる 生成されたファイルがビルドの一部になる
5. コードが利用可能に! 生成されたメソッド/クラスが呼び出せるようになる

5. デバッグとよくあるミス

ジェネレータを書くときの初心者に多いミスの一つは、コードを追記したいクラスに partial キーワードを付け忘れることです。partial を付けないとコンパイラはあなたの変更を認識しません。生成されたファイルが IDE に反映されるまで最初のリビルドが必要なこともあるので慌てないでください。

生成ファイルの名前付けにも注意してください:すべて同じ名前にするとファイルが上書きされます。ハックとしては、生成ファイル名に対象クラス名を含めるとよいです — 例えば context.AddSource($"{className}_ToString", ...) のように。

属性の二重インポートエラーにも注意:生成されたクラスに既に存在する属性を再生成すると競合が発生します。必要な属性は共通プロジェクトに移すか、必要な場合にのみ生成するのが良いです。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION