|
|
連載:C# 4入門
第7回 DynamicObjectを継承したダイナミックなクラス
株式会社ピーデー 川俣 晶
2011/01/14 |
|
|
ダイナミックに解決せよ!
実は、いくつかの条件を追加すると、この式は最初の例と同じ長さに抑え込むことができる。
DataStore.Data["A"]
= (int)DataStore.Data["B"] + (int)DataStore.Data["C"];
|
|
dataStore.A = dataStore.B + dataStore.C;
|
|
その条件とは以下のとおりである。
- DynamicObject型を使用する
- dynamic型でアクセスする
まずはソース・コードを見ていただきたい。
using System;
using System.Collections.Generic;
using System.Dynamic;
public static class Work
{
public static void DoIt(dynamic dataStore)
{
dataStore.A = dataStore.B + dataStore.C;
}
}
public class DataStore : DynamicObject
{
private Dictionary<string, object> data
= new Dictionary<string, object>();
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (data.TryGetValue(binder.Name, out result)) return true;
return false;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
data[binder.Name] = value;
return true;
}
}
class Program
{
static void Main(string[] args)
{
dynamic dataStore = new DataStore();
dataStore.B = 1;
dataStore.C = 2;
Work.DoIt(dataStore);
Console.WriteLine(dataStore.A);
}
}
|
|
リスト3 |
ではコードを解説しよう。
System.Dynamic名前空間のDynamicObjectクラスを継承したオブジェクトは、任意のメンバを動的に作成して持つことができる。ただし、これらのメンバはdynamic型を経由してアクセスした場合にのみアクセスできる。コンパイル時に解決できない名前だからである。
実際のアクセスは、取得はTryGetMemberメソッド、書き込みはTrySetMemberメソッドをオーバーライドして扱う。リスト3では、そのままDictionaryオブジェクトのアクセスに置き換えているが、別の内容でももちろん構わない。そこを自由に扱えるのがDynamicObjectクラスである。そして、アクセスされたメンバがあればtrueを返し、なければfalseを返す。これで、存在しない名前にアクセスされても、きちんとエラーを出せる。
ちなみに指定された名前はbinder.Nameプロパティで取得できる。AやB、Cは、このプロパティで名前を調べることができる。
この機能は便利か?
もう1つの解決方法があるのだが、それは次回に解説を行う予定である。
さて、この方法は便利かといえば、そこは微妙だ。
例えば、以下の書き換えは単純に短くなったと喜んでよい。機能的な欠損は何もないからである。
DataStore.Data["A"]
= (int)DataStore.Data["B"] + (int)DataStore.Data["C"];
|
|
dataStore.A = dataStore.B + dataStore.C;
|
|
Aの名前を書き間違えてもコンパイル時にエラーにならないのも同じ。型が正しくないと実行時にエラーになるのも同じである。
しかし、最初の例と比較すると機能が落ちている。
DataStore.A = DataStore.B + DataStore.C;
|
|
dataStore.A = dataStore.B + dataStore.C;
|
|
最初の例では、名前と型のチェックがコンパイル時に実行されるが、後者は実行時までミスの発覚が遅れてしまう。だから、いつでもこの機能を利用することはお勧めしない。それなしで書けるのなら、それに越したことはないからだ。
しかし、何があるのか実行する段階まで分からない動的な世界でプログラミングを行う場合は、冗長性をソース・コードから除去できる可能性がある。リフレクションなどを使用して回りくどいコードを書くよりも、ずっとすっきりするはずである。
動的なメソッド
一連の数値を並べて出力したいとしよう。
よくあるパターンだと、以下のように書いてしまうだろう。
using System;
class Program
{
static void Main(string[] args)
{
int[] ar = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
foreach (var n in ar) Console.WriteLine(n);
}
}
|
|
リスト4 |
しかし、{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; といった記述が多くなると、もっと短く書けないかと思うかもしれない。たまた数値に規則性があるので、この例を短くすることは簡単だが、ここは「規則性はない」という前提で進めよう。
このような場合はプログラムの内部でメソッド名を動的に評価するとして、メソッド名に値をすべて埋め込んでしまえばよい。
using System;
using System.Dynamic;
using System.Linq;
public class Order : DynamicObject
{
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
result = binder.Name.Substring(1).Split('x')
.Select((c) => int.Parse(c));
return true;
}
}
class Program
{
static void Main(string[] args)
{
dynamic order = new Order();
var ar = order.N1x2x3x4x5x6x7x8x9();
foreach (var n in ar) Console.WriteLine(n);
}
}
|
|
リスト5 |
この場合、(ほぼ)対応する行を比較すると以下のようになる(ただしarは配列、eは列挙オブジェクトで型は異なる)。
int[] ar = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
|
|
var e = order.N1x2x3x4x5x6x7x8x9();
|
|
名前に埋め込まれた数値は、ダイナミック・オブジェクトで評価されることになる。TryInvokeMemberメソッドは、呼ばれたメソッドの名前に関係なく呼び出されるので、ここで名前から数値を抜き出すことができる。
さて、この機能を使うと本当に便利かというと、これも微妙だ。やはり、使わないで済むなら使わない方がよいだろう。しかし、まれに名前によって動作を変えるメソッドが記述できるとソース・コードがコンパクトになる事例もあるので、そういう場合は長々とリフレクションを記述するよりも好ましいかもしれない。