リフレクションとメタプログラミング
概要
メタプログラミングは、プログラムが自分自身や他のプログラムを操作・生成する技術です。.NETでは、リフレクション、Expression Tree、そして近年注目されているSource Generatorなど、強力なメタプログラミング機能が提供されています。
リフレクション (Reflection)
リフレクションは、実行時にアセンブリ、モジュール、型、メンバーの情報を取得・操作する機能です。
基本的な使い方
using System.Reflection;
public class Person
{
public string Name { get; set; }
public void SayHello() => Console.WriteLine($"Hello, {Name}!");
}
// 型情報の取得
Type type = typeof(Person);
// または
// Person p = new Person();
// Type type = p.GetType();
// インスタンスの生成
object instance = Activator.CreateInstance(type);
// プロパティの設定
PropertyInfo prop = type.GetProperty("Name");
prop.SetValue(instance, "Alice");
// メソッドの実行
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(instance, null); // 出力: Hello, Alice!
パフォーマンスへの影響と対策
リフレクションは強力ですが、通常のコード実行に比べて低速です。
- キャッシュ:
PropertyInfoやMethodInfoは取得コストが高いため、キャッシュして再利用します。 - デリゲート:
Delegate.CreateDelegateを使用して、リフレクション呼び出しをデリゲートに変換することで高速化できます。
Source Generator
Source Generatorは、コンパイル時にコードを解析し、新しいC#ソースコードを生成してコンパイルに追加する機能です。.NET 5から導入されました。
リフレクションとの違い
- 実行時 vs コンパイル時: リフレクションは実行時に解決しますが、Source Generatorはコンパイル時にコードを生成するため、実行時のオーバーヘッドがありません。
- AOT (Ahead-of-Time) 対応: リフレクションに依存しないため、Native AOTなどの環境に適しています。
ユースケース
System.Text.Json: JSONシリアライズの高速化ILogger: 高速なロギングメソッドの生成INotifyPropertyChanged: ボイラープレートコードの自動生成
Expression Tree (式ツリー)
Expression Treeは、コードをデータ構造(ツリー)として表現したものです。これにより、コードを実行時に検査、変更、または実行可能なコードにコンパイルすることができます。
基本
using System.Linq.Expressions;
// ラムダ式をデータとして扱う
Expression<Func<int, int, int>> addExpr = (a, b) => a + b;
// コンパイルして実行可能なデリゲートにする
Func<int, int, int> addFunc = addExpr.Compile();
Console.WriteLine(addFunc(1, 2)); // 3
ORMでの活用
Entity Framework CoreなどのLINQプロバイダは、Expression Treeを使用してC#のクエリ式をSQLに変換しています。
Dynamic型とExpandoObject
dynamic型
C# 4.0で導入された dynamic キーワードを使用すると、型チェックを実行時まで遅延させることができます。
dynamic d = 1;
Console.WriteLine(d.GetType()); // System.Int32
d = "Hello";
Console.WriteLine(d.GetType()); // System.String
ExpandoObject
System.Dynamic.ExpandoObject を使用すると、実行時にメンバーを動的に追加・削除できるオブジェクトを作成できます。
dynamic person = new System.Dynamic.ExpandoObject();
person.Name = "Bob";
person.Age = 30;
// 辞書としてキャスト可能
var dict = (IDictionary<string, object>)person;
Console.WriteLine(dict["Name"]); // Bob
ベストプラクティス
- リフレクションよりSource Generatorを優先する: パフォーマンスとAOTの互換性のために、可能な限りコンパイル時のコード生成を使用してください。
- リフレクション結果をキャッシュする: 型情報やメンバー情報は頻繁に変更されないため、取得した
MethodInfoやPropertyInfoは静的フィールドや辞書にキャッシュしてください。 dynamicの使用を最小限にする: 静的型付けの安全性とIDEのサポート(IntelliSenseなど)を失うため、必要な場合(例:COM連携、動的JSON処理)以外は避けてください。- セキュリティに配慮する: リフレクションを使用してプライベートメンバーにアクセスすることは可能ですが、セキュリティリスクや将来の互換性問題を引き起こす可能性があるため、慎重に行ってください。
まとめ
- リフレクション: 汎用性が高いが、パフォーマンスに注意が必要。
- Source Generator: コンパイル時にコード生成を行うため、パフォーマンスとAOT対応に優れる。現代の.NET開発ではリフレクションの代替として推奨されることが多い。
- Expression Tree: LINQプロバイダや動的なクエリ構築に不可欠。
- Dynamic: 静的型付けの恩恵を捨てることになるため、COM相互運用や動的言語との連携など、必要な場面に限定して使用する。