プログラミング C# - 翔ソフトウェア (Sho's)

C#/.NET/ソフトウェア開発など

DynamicObject を使ってみよう

Dynamic

C# 4 から dynamic が使えるようになった。

動的言語のように、動的にプロパティを参照したり、メソッドを呼んだり出来るようになった訳だ。

そして、.NET Framework では 4 から System.Dynamic と云う名前空間ができた。

今回は、この名前空間の中の DynamicObject を使ってみたい。

■ DynamicObject を使った例

・DynamicObject

DynamicObject は、メソッド呼び出し、プロパティへのアクセス、インデックスによるアクセス等のそれぞれに対応する virtual メソッドを持つ。

例.

virtual メソッド名 説明
TrySetMember オーバーライドして、プロパティに値が設定されるときの動作を定義できる
TryGetMember オーバーライドして、プロパティから値が取得されるときの動作を定義できる
TrySetIndex オーバーライドして、インデックスを用いて値が設定されるときの動作を定義できる
TryGetIndex オーバーライドして、インデックスを用いて値が取得されるときの動作を定義できる
TryInvokeMember オーバーライドして、メソッドが呼び出されるときの動作を定義できる

DynamicObject から派生したクラスを作り、これらの virtual メソッド をオーバーライドすることで、動的な動作を定義することができる。

・DynamicObjectを使った例 1

実際に試してみよう。

今回は、TrySetMember と TryGetMember をオーバーライドしてプロパティにアクセスしてみる。

using System;
using System.Dynamic;

class Dynamic : DynamicObject
{
    // プロパティに値を設定しようとしたときに呼ばれる
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        return base.TrySetMember(binder, value);
    }

    // プロパティから値を取得しようとしたときに呼ばれる
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        return base.TryGetMember(binder, out result);
    }
}

class Program
{
    static void Main()
    {
        dynamic item = new Dynamic();
        item.Id = 100; // 実行時エラー (Id の定義がない)
        Console.WriteLine(item.Id); // 実行時エラー (Id の定義がない)
        item.Name = "田中一郎"; // 実行時エラー (Name の定義がない)
        Console.WriteLine(item.Name); // 実行時エラー (Name の定義がない)
    }
}

Visual Studio を使って、オーバーライドした TrySetMember と TryGetMember にブレークポイントを置いてデバッグ実行してみると、それぞれプロパティの設定時と取得時にちゃんと呼ばれていることが判る。

DynamicObjectを使った例 1 - ブレークポイントで止めたところ
DynamicObjectを使った例 1 - ブレークポイントで止めたところ

base.TrySetMember(binder, value) と base.TryGetMember(binder, out result) は false を返している。

TrySetMember が false を返すとプロパティの設定に失敗し、TryGetMember が false を返すとプロパティの取得に失敗する。

従って、このプログラムを実行してみると、以下のような実行時エラーになる。

DynamicObjectを使った例 1 - 実行時エラー
DynamicObjectを使った例 1 - 実行時エラー
・DynamicObjectを使った例 2

TrySetMember と TryGetMember の中をもう少しちゃんと実装して、動くようにしてみよう。

TrySetMember の中で、Dictionary を使って設定した値を覚えるようにしてみる。

そして、同じプロパティが再設定されるときは、型が同じか派生クラスのときだけ許すようにしてみる
(この実装はやや妥当ではないが、今回はこうしてみる)。

そして、TryGetMember の中では、Dictionary から値を取り出して返すようにしてみよう。

こんな感じだ。

using System;
using System.Collections.Generic;
using System.Dynamic;

class Dynamic : DynamicObject
{
    // observableTarget の該当するプロパティ名毎に型と値を格納
    Dictionary<string, Tuple<Type, object>> values = new Dictionary<string, Tuple<Type, object>>();

    // プロパティに値を設定しようとしたときに呼ばれる
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        Tuple<Type, object> tuple;
        // 該当するプロパティの型と値を values から取得
        if (values.TryGetValue(binder.Name, out tuple)) {
            var valueType = value.GetType();
            // もしプロパティに設定しようとしている値の型が、そのプロパティの型もしくはそのサブクラスでなかったら
            if (!valueType.Equals(tuple.Item1) && !valueType.IsSubclassOf(tuple.Item1))
                return false;
            // 元の型の儘で値を再設定
            values[binder.Name] = new Tuple<Type, object>(tuple.Item1, value);
            return true;
        }
        // 型と値を新規に格納
        values[binder.Name] = new Tuple<Type, object>(value.GetType(), value);
        return true;
    }

    // プロパティから値を取得しようとしたときに呼ばれる
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        Tuple<Type, object> tuple;
        // 該当するプロパティの型と値を values から取得
        if (values.TryGetValue(binder.Name, out tuple)) {
            result = tuple.Item2;
            return true;
        }
        result = null;
        return false;
    }
}

class Program
{
    static void Main()
    {
        dynamic item = new Dynamic();
        item.Id = 100; // OK
        Console.WriteLine(item.Id); // OK
        //item.Id = "田中一郎"; // 実行時エラー (異なった型の値を設定しようとしている)
        item.Name = "田中一郎"; // OK
        Console.WriteLine(item.Name); // OK
        //Console.WriteLine(item.Address); // 実行時エラー (設定していないプロパティの値を取得しようとしている)
    }
}

今度の実行結果は次の通り。

100
田中一郎

item.Id に異なった型の値を設定しようとしたり、設定していないプロパティの値を取得しようとしたりしている箇所は、実行時エラーになる。
それ以外は正常に動く。

プロパティの再設定時に、再設定する値の型が派生クラスなら設定できるが派生クラスでなければ設定できないことも、確認しておこう。

class Super {}

class Sub : Super {}

class Program
{
    static void Main()
    {
        dynamic item = new Dynamic();

        item.Property1 = new Super(); // OK
        item.Property1 = new Sub(); // OK

        item.Property2 = new Sub(); // OK
        item.Property2 = new Super(); // 実行時エラー (型が違うし、派生クラスでもない)
    }
}

最後の、型が同じでも派生クラスでもなかったときだけ実行時エラーとなる。

■ ExpandoObject を使った例

次に、同じ名前空間 System.Dynamic の中の ExpandoObject も試してみよう。

DynamicObject は派生して使うが、ExpandoObject は派生せずにその儘使う。

こんな感じだ。

using System;
using System.Dynamic;

class Program
{
    static void Main()
    {
        dynamic item = new ExpandoObject();

        item.Id = 100; // OK
        Console.WriteLine(item.Id); // OK
        item.Id = "田中一郎"; // OK
        Console.WriteLine(item.Id); // OK
        item.Name = "田中一郎"; // OK
        Console.WriteLine(item.Name); // OK
        //Console.WriteLine(item.Address); // 実行時エラー(設定していないプロパティの値を取得しようとしている)
    }
}

実行結果は次の通り。

100
田中一郎
田中一郎

ExpandoObject はその儘で、動的にプロパティを設定したり、取得したりできる。

こちらは、最後の設定してないプロパティを取得しようとしたとき以外は、実行時エラーにならない。

■ 今回のまとめ

今回は、DynamicObject を使ってみた。

近く、これの応用例として、DynamicObject をオブザーバー パターンの実装に利用してみる予定だ。

お楽しみに。