この記事は .NET 5 を使いたい理由6選(ASP.NET Core編) のシリーズ続編ですが、前記事からは独立した内容です。 |
ソースジェネレーターを使いたい
ソースジェネレーターというとC#にはT4(Text Template Transformation Toolkit)テキストテンプレートと呼ばれるものが昔からありました。これはVisual Studio 2005のときに登場した汎用ソースジェネレーターで、実にC#2.0のころから存在しています。(Visual Studio 2008 から同梱されるようになりました。)
T4は標準でインストールされることと、ビルドプロセスに容易に組み込めることから、C/C++におけるプリプロセッサーの如く長く活用されてきました。
この度.NET 5(C#9)には新たにソースジェネレーターの機能が追加されました。今更なぜこれが必要だったのでしょうか。
今回は使いたい理由を考えながらC#9ソースジェネレーターとT4を比較し、使うべき局面を見極めていきましょう。
使いたい理由1 : 定型的な記述を減らせる
恐らく、皆さんの中にも無数のアイデアがあるでしょう。私の中にも沢山あります。
- 通知型プロパティの実装(WPFのViewModelなどのINotifyPropertyChanged実装を簡素化できる)
- 綺麗なValueObject型の実装(適切にシリアライズできるようにするのはこれまで大変だった)
- シリアライザを用いないシリアライズ/デシリアライズ
- CSVやXMLやJSONからのC#生成、またはその逆(DB等のシード生成や手書きのreadonly構造体の代替に良いと思える)
- partialメソッドの自動実装
- Equals(等値比較)/Deep Copy(内部プロパティを含んだコピー)などの自動実装
- structをinterfaceとして使う際にbox化しないジェネリックメソッドの自動生成
- DIコンテナへの登録の自動化/DIコンテナ自身の自動生成
- コンバーターの自動生成
などなど、これらにとどまらず可能性はもっとたくさんあると考えます。
このうち、いくつかはT4向きですし、いくつかはソースジェネレーター向きです。
現時点ではっきりしているのは、Windowsデスクトップアプリケーション/ASP.NET Core/EF Core等のORM/基盤ライブラリなどのいずれの領域においてもソースの自動生成は役立つケースがあるということです。
使いたい理由2 : IntelliSenseに対応している
これはまさにRoslyn(公式のC#コンパイラープロジェクト)の恩恵ですが、私たちはソースコードを記述している最中、ビルドせずともIntelliSenceや各種アナライザ恩恵を受けることができます。また、アナライザを登録したり設定を変えたりすることによってソース解析や警告をエディター上に表示することができます。
そして、C#9ソースジェネレーターはアナライザとしてプロジェクトに登録します。
つまり、実際のコンパイルプロセスとIntelliSenceの処理のどちらにも同じソースコードを挿入する仕掛けとなっています。
T4の場合、あくまでカスタムビルドとしての動作であったため、ソースコードの内容に応じたコード出力はコンパイル時のみとなります。IntelliSenceに反映できるのはコンパイルが成功した後です。
このため、T4の生成コードを使用する部分が関連モジュール内にあると、やや難解なコンフリクトを生じることがありました。
ソースジェネレーターではそういうことはありません。
使いたい理由3 : T4と併用できる
このソースジェネレーターがT4に取って代わるものとして生まれてこなかったことはとても良いことです。
良い理由の1つは用途に応じて使い分けができること。
もう1つは、ある程度以上の規模のソースジェネレーターについては、T4併用が良いと考えられることです。
ここでRoslyn本家のプロジェクトに掲載されているサンプルの一部を読んでみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
namespace SourceGeneratorSamples { [Generator] public class AutoNotifyGenerator : ISourceGenerator { private const string attributeText = @" using System; namespace AutoNotify { [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] sealed class AutoNotifyAttribute : Attribute { public AutoNotifyAttribute() { } public string PropertyName { get; set; } } } "; public void Initialize(GeneratorInitializationContext context) { // Register a syntax receiver that will be created for each generation pass context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); } public void Execute(GeneratorExecutionContext context) { // add the attribute text context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8)); |
このサンプルは簡素な構成で動作を伝えるというポイントはクリアしています。ですが、私たちがこのサンプルをそのまま流用して開発することは望ましくありません。
なぜなら出力するコードがむき出しの文字列として扱われており、記述もメンテナンスも困難だからです。 (誰しもがこの部分にシンタックスハイライトやIntelliSenceを欲するでしょう。)
しかしT4とのハイブリッド構成であれば、Visual StudioのT4用の拡張を用いればエディターの各種補助を受けることができます。
例えば上記のAutoNotifyAttributeなどは.csファイルに記述した上でT4からインクルードして、テキストとして扱いつつもエディターの支援を受けることができるようになります。T4の機能が必要な部分だけをpartialクラスとして切り出せば良いのです。
1 |
private const string attributeText = @"<#@ include file="AutoNotifyAttribute.cs" #>"; |
実際のコードサンプルは以下のリンク先にあります。
https://github.com/cct-blog/cct-reki-yaymamoto/tree/main/SourceGeneratorSample/Oniqys.Wpf.Generator
もちろん、すべての個所で通用するわけではありませんが、この手法はソースジェネレーター開発を比較的簡単なものとしてくれます。
メモ 筆者はT4のユースケースとして.NET のモジュールのメタ情報を用いた自動ソース出力を多く見てきました。しかしT4は汎用ですのでどのような情報からでもC#を出力することができます。静的なソース出力が欲しい場合、T4を使う方が効率的です。「使いたい理由1」に挙げた「CSVやXMLやJSONからのC#生成」のようなものは、大抵T4の方が適しています。 上記リンク先の例はあくまで動的なものに静的コードを挿入する場合に役立つTipsです。 |
使いたい理由4 : メタ情報とリフレクションとBox化を回避した高速なコードを出力できる
これは、クラスの外部にコード生成することによる実装簡略化の戦略です。
C#のstructにはinterfaceを付けることができますが、コンパイラーが特殊扱いしている一部ケースを除き、structをinterfaceにキャストするとBox化してパフォーマンスを損ないます。
とはいえ、汎用性の高いコードは愛すべきもので、同じコードをいくつも記述することは厭わしいものです。型毎に同じコードを書くことは多くの点で開発を厄介なものにします。
この問題の大部分を解消するために私たちはジェネリックコードを利用することができます。
しかし、欲しいinterfaceを持たないstruct/classについてはどうしたものか、頭を悩ますことはあるでしょう。
そこでソースジェネレーターです。欲しいinterfaceを持っていないが、同名のプロパティやメソッドを持っている場合に、interfaceによらずパターンに基づいたコードを生成します。
元のロジックはinterfaceを使って記述し、そのinterfaceと同じプロパティ/メソッドを持つクラスや構造体に等価のルーチンを自動生成し提供します。
これまで手で書いてきたものを自動化するというアプローチの一つにすぎませんが、これには大きな意味があります。
使いたい理由5 : AOP(アスペクト指向プログラミング)が容易になる
これは、言わばクラスの内部にコード生成することによる実装簡略化の戦略です。
DI(依存性注入/Dependency Injection)と絡めることは前提となりますが、質の良いProxyクラスを間に挟むことが可能になります。
事前処理・事後処理をメソッドに付与することを自動化できることには大きな意味があります。 また、特定のパターンを持つクラスにinterfaceを付与することなど、 Proxyクラスでは、考えうるあらゆることが暗黙的に可能になります。
こうしたことを今まで積極的に行わなかった理由の1つとして、手書きのProxyクラスのメンテナンス性の低さが挙げられます。しかし、ソースジェネレーターがあるならその問題は多くの点で緩和されます。
メモ ソースジェネレーターによって暗黙的に生成されたコードを見たいときは、暗黙的に生成されたもの(メソッドやプロパティなど)をエディター上で右クリックして「定義へ移動」すると、生成されたコードを表示することができます。 |
使いたい理由6 : XMLDocumentを付与することが簡単にできる
最後に、ソースジェネレーターを使いたい最も重要な点を書きたいと思います。
自動生成したコードをIntelliSenceで見たとき、型と名称しか出てこないものに何の魅力があるでしょうか。
しかしC#9のソースジェネレーターは構文ツリーの解釈をしてるため、元のソースコードのコメントを生成先のコードに転用することが可能です。
各シンボルが持つ GetDocumentationCommentXml() メソッドを用いればクラスやメンバーのXML Documentationを生成することもできます。
今までの自動生成コードの多くが苦手としていたところを容易に解決できるのです。
サンプルコードの全容はこちらです。
https://github.com/cct-blog/cct-reki-yaymamoto/tree/main/SourceGeneratorSample
ポイント : 乱用には注意
C/C++やJavaには俗に黒魔術と呼ばれる領域があります。
例えばJavaはエンジニア自身が書いているコードからは全く見えないところに、暗黙的な処理を追加することができますし、多くの便利なフレームワークがアノテーションを見ながら様々な処理を追加しているのをJavaエンジニアの皆さんはご存じでしょう。
C/C++にも複雑を極める領域があり、使い方次第で熟練者でも手を焼くコードを書くことができてしまいます。
このC#9ソースジェネレーターもそういったことを可能にする類のものです。使いどころを十分に検討することはとても大切です。
まとめ
- 定型的のコードを書かなくてよい
- エディターの支援をすべて受けることができる
- T4との親和性が高い
- インターフェースによらずパターンによるコード生成を実現する
- AOPやProxyクラスがより身近になる
- 自動生成コードに有機的なコメントを付与することができる
このすべてが、C#9のソースジェネレーターを使いたい理由です。
感謝
.NET 5/C#の発展に寄与しておられるすべてのエンジニアに感謝いたします。