こんにちは、働くC#プログラマーのさんさめです。
DynamicObjectをご存じでしょうか。
DynamicObjectはその名の通り、
動的な操作をサポートするメソッドを持った
抽象クラスです。
たとえば、
TryGetMember
TrySetMember
をoverrideすることで、
動的なプロパティを実現することができます。
しかし、この動的なプロパティに、
文字列ベースでアクセスしようと思うと、
C#上ではなかなか難しいです。
(WPFだったらBindingを作ってしまえば
良いので簡単なのですが…)
DynamicObjectの動的プロパティに、
文字列でアクセスできるようにするためには、
大きく分けて2つの方法があります。
- TryGetIndexを実装する
- GetMemberBinderクラスの具象クラスを自前で作る
詳しく解説していきます。
DynamicObjectの動的プロパティの基本アクセス方法
まず、DynamicObjectを使って、
動的なプロパティを実現するためには、
以下のようなクラスを作ります。
class MyDynamic : DynamicObject
{
Dictionary<string, object> DynamicPropDic { get; }
= new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var propertyName = binder.Name;
if (DynamicPropDic.TryGetValue(propertyName, out result))
{
return true;
}
return base.TryGetMember(binder, out result);
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
DynamicPropDic[binder.Name] = value;
return true;
}
}
このクラスを動的に使いたい場合は、
以下のようになります。
dynamic myDynamic = new MyDynamic();
myDynamic.Hoge = "aa";
Console.WriteLine(myDynamic.Hoge);
MyDynamicクラスには、
Hogeというプロパティはありませんが、
TryGetMember/TrySetMemberを実装しているおかげで、
普通に実行できてコンソールには「aa」が出力されます。
しかし、この方法で動的なプロパティに
アクセスしたい場合、
必ずdynamic型にキャストして、
プロパティのようにアクセスしなくてはいけません。
つまり、以下のように書くことはできません。
dynamic myDynamic = new MyDynamic();
var propertyName = "Hoge";
// propertyNameに代入された文字列で
// アクセスしたいが…(「Hoge」プロパティにアクセスしたい)
// ↓では、「propertyName」というプロパティにアクセスしたことになってしまう
myDynamic.propertyName = "aa";
Console.WriteLine(myDynamic.propertyName);
このように外部から読みこんだ文字列で
プロパティを取得・設定したい場合、
方法は大きく2つあります。
方法1.[]を使えるようにTryGetIndex,TrySetIndexをoverrideする
正攻法です。
DynamicObject継承クラス側で、
TryGetIndexとTrySetIndexの2つをoverrideします。
このメソッドはインデクサによってアクセスしたときに、
呼ばれるメソッドです。
例えば文字列でアクセスできるようにしたい場合は、
以下のように実装します。
public override bool TrySetIndex(SetIndexBinder binder, object[] indexes, object value)
{
if (indexes.Length == 1
&& indexes[0] is string str)
{
DynamicPropDic[str] = value;
return true;
}
return base.TrySetIndex(binder, indexes, value);
}
public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
{
if (indexes.Length == 1
&& indexes[0] is string str
&& DynamicPropDic.TryGetValue(str, out result))
{
return true;
}
return base.TryGetIndex(binder, indexes, out result);
}
上記のようにTryGetIndex/TrySetIndexを実装すると、
使う側では、Dictionaryっぽく
アクセスできるようになります。
dynamic myDynamic = new MyDynamic();
var propertyName = "Hoge";
myDynamic[propertyName] = "aa";
Console.WriteLine(myDynamic[propertyName]);
これで、「Hoge」のところが、
外部からの入力(ex. コマンドライン引数)
になっても対応できます。
しかし、この方法の欠点として、
「DynamicObject実装クラスが
自分の管理下にある場合」
しか実現できません。
自分が手を出せない場所にある
クラスだった場合は、
そもそも追加実装ができませんよね。
でも、諦める必要はありません。
もう1つの方法を使えば、
TryGetIndexを実装できなくても、
文字列ベースでアクセスできるようになります。
方法2.GetMemberBinderを継承してTryGetMemberを直接呼ぶ
目的のクラスが、
DynamicObject継承クラスである場合、
TryGetMemberを自前で呼んでしまえばよいのです。
TryGetMember(GetMemberBinder binder, out object result);
となっています。
このとき、resultはただの受け皿なので、
binder変数さえ用意できれば良いという事になります。
binder変数は、GetMemberBinder
という抽象クラスなのですが…、
どうやら、組み込みの具象クラスはなさそうです。
では、やはり呼び出せないのでしょうか?
いえいえ、よくよく見ると
実装すべきメソッドは2つだけです。
というわけで、
このGetMemberBinderを継承した、
自前クラスを作ってしまいましょう。
TrySetMemberも同様の考え方で実現できます。
それぞれの具象クラスのサンプルが以下です。
class MyGetMemberBinder : GetMemberBinder
{
public MyGetMemberBinder(string name) : base(name, false)
{
}
public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
{
throw new NotImplementedException();
}
}
class MySetMemberBinder : SetMemberBinder
{
public MySetMemberBinder(string name) : base(name, false)
{
}
public override DynamicMetaObject FallbackSetMember(DynamicMetaObject target, DynamicMetaObject value, DynamicMetaObject errorSuggestion)
{
throw new NotImplementedException();
}
}
大文字小文字を無視したいケースはなかったので、
プロパティ名だけを受け取って
インスタンスを作れるようにしました。
また、FallbackGetMemberメソッドに関しては、
このユースケースにおいては
意識する必要がないので、
未実装のままにしています。
実際にこれを使って動的アクセスするコードは
以下のようになります。
object myDynamic = new MyDynamic();
var propertyName = "Hoge";
if (myDynamic is DynamicObject dynamicObject)
{
dynamicObject.TrySetMember(new MySetMemberBinder(propertyName), "aa");
if (dynamicObject.TryGetMember(new MyGetMemberBinder(propertyName), out var result))
{
Console.WriteLine(result);
}
}
動的アクセスしたいインスタンスが、
DyanmicObject継承クラスだった場合は、
先ほど定義した自前クラスを使って、
TrySetMember、およびTryGetMemberを、
直接実行しています。
まとめ
まとめです。
- DynamicObjectに文字列で動的アクセスしたい場合
TryGetMember, TrySetMemberだけだとうまくいかない - TryGetIndex, TrySetIndexを実装すると、
インデクサを使ってアクセスできるようになる - TryGetIndexを実装できない場合は、
GetMemberBinderを自前で継承してしまえば、
TryGetMemberを直接呼び出すことが可能
最後までお読みいただき、ありがとうございました。
コメント