C#は静的型付け言語ですが、dynamicキーワードとSystem.Dynamic.DynamicObjectクラスを利用することで、実行時に動的にプロパティを追加・取得するクラスを作成できます。
これは、JSONのようなスキーマレスなデータを扱う場合や、コンパイル時には構造が決定していないデータを柔軟に操作したい場合に有効な手法です。ここでは、辞書(Dictionary)を内部ストレージとして持ち、プロパティ構文(obj.Property = value)でアクセスできる動的なデータクラスの実装方法を解説します。
DynamicObjectの概要
DynamicObjectクラスは、動的操作の動作を定義するための基底クラスです。このクラスを継承し、以下のメソッドをオーバーライドすることで、動的な振る舞いをカスタマイズできます。
- TrySetMember: プロパティへの値の代入時に呼び出されます。
- TryGetMember: プロパティからの値の取得時に呼び出されます。
実践的なコード例:動的プロパティストアの実装
以下のコードは、DynamicObjectを継承し、任意のプロパティを自由に追加できるDynamicPropertyStoreクラスの実装例です。内部的にはDictionary<string, object>でデータを管理しています。
using System;
using System.Collections.Generic;
using System.Dynamic;
namespace DynamicProgramming
{
class Program
{
static void Main()
{
// dynamic型としてインスタンスを生成します。
// これにより、コンパイラの型チェックをバイパスし、実行時にメンバー解決が行われます。
dynamic userProfile = new DynamicPropertyStore();
// 1. プロパティを動的に設定(TrySetMemberが呼ばれる)
userProfile.FullName = "佐藤 一郎";
userProfile.Address = "東京都千代田区";
userProfile.BirthDate = new DateTime(1990, 5, 15);
userProfile.Age = 33;
// 2. プロパティの値を取得(TryGetMemberが呼ばれる)
Console.WriteLine($"名前: {userProfile.FullName} ({userProfile.FullName.GetType().Name})");
Console.WriteLine($"住所: {userProfile.Address}");
Console.WriteLine($"誕生日: {userProfile.BirthDate:yyyy/MM/dd}");
// 3. 独自メソッドの利用(dynamic型でもキャストすれば通常のメソッドを呼び出せます)
// または、インターフェースを介して呼び出す設計も可能です。
var store = (DynamicPropertyStore)userProfile;
if (store.HasProperty("Address"))
{
Console.WriteLine("「Address」プロパティは定義済みです。");
}
}
}
/// <summary>
/// 実行時にプロパティを動的に追加・取得できるクラス
/// </summary>
public class DynamicPropertyStore : DynamicObject
{
// プロパティ名と値を保持する辞書
private readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
/// <summary>
/// プロパティに値を設定する際に呼び出されます。
/// </summary>
/// <param name="binder">設定しようとしているメンバーの情報</param>
/// <param name="value">設定する値</param>
/// <returns>処理が成功した場合はtrue</returns>
public override bool TrySetMember(SetMemberBinder binder, object value)
{
// プロパティ名をキーとして辞書に値を保存
// 既に存在する場合は上書き、なければ追加されます。
_properties[binder.Name] = value;
return true;
}
/// <summary>
/// プロパティから値を取得する際に呼び出されます。
/// </summary>
/// <param name="binder">取得しようとしているメンバーの情報</param>
/// <param name="result">取得された値(出力引数)</param>
/// <returns>値の取得に成功した場合はtrue</returns>
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
// 辞書から値を取得を試みる
return _properties.TryGetValue(binder.Name, out result);
}
/// <summary>
/// 指定されたプロパティが定義されているか確認します。
/// </summary>
public bool HasProperty(string name)
{
return _properties.ContainsKey(name);
}
}
}
実行結果
名前: 佐藤 一郎 (String)
住所: 東京都千代田区
誕生日: 1990/05/15
「Address」プロパティは定義済みです。
技術的なポイント
1. TrySetMemberの実装
binder.Nameには、アクセスされたプロパティ名(例:”FullName”)が入ります。これをキーとして内部のDictionaryを更新することで、動的なプロパティ追加を実現します。戻り値としてtrueを返すことで、ランタイムに対して「処理が正常に行われた」ことを伝えます。
2. TryGetMemberの実装
値を取得する際は、outパラメータであるresultに値をセットし、戻り値としてtrueを返します。もし辞書にキーが存在せず、値を返せない場合はfalseを返すと、呼び出し元でRuntimeBinderExceptionが発生します(C#のデフォルトの挙動)。
3. dynamic変数の利用
このクラスを利用する際は、変数の型をdynamicにする必要があります。
DynamicPropertyStore obj = new DynamicPropertyStore();
obj.Name = "Test"; // コンパイルエラーになります(DynamicPropertyStoreにNameプロパティがないため)
dynamic obj = new DynamicPropertyStore();
obj.Name = "Test"; // 正常に動作します(TrySetMemberが呼ばれる)
まとめ
DynamicObjectを継承することで、C#上でもPythonやJavaScriptのような柔軟なオブジェクト操作が可能になります。DTO(Data Transfer Object)の構造を動的に決定したい場合や、外部スクリプト言語との相互運用を行う際などに、非常に強力なツールとなります。
