DynamicObjectの動的プロパティに文字列でアクセスする

C#

こんにちは、働く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の定義

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を直接呼び出すことが可能

最後までお読みいただき、ありがとうございました。

コメント

タイトルとURLをコピーしました