Cutting Edge
C# 4.0 の Expando オブジェクト
Dino Esposito
Microsoft .NET Framework 向けに記述されるコードの大半は、.NET でリフレクションによる動的型指定がサポートされているにもかかわらず、静的型指定に基づいています。さらに、JScript では、Visual Basic と同様、10 年前に .NET の上位に動的型システムが導入されました。静的型指定とは、すべての式の型が既知であることを意味します。型や割り当てがコンパイル時に検証され、考えられる型指定エラーの大半が事前にキャッチされます。
よく知られている例外は、実行時にキャストを試みる場合です。この場合は、ソースの型とターゲットの型に互換性がないと、動的エラーが発生する可能性があります。
静的型指定は、パフォーマンスやわかりやすさの点で優れていますが、コード (およびデータ) についてほぼすべてをあらかじめ理解しているという前提に基づいています。現在では、このような制約を少しでも緩和することが強く求められています。静的型指定では対応できない場合は、通常、動的型指定、動的オブジェクト、および間接プログラミングまたはリフレクション ベースのプログラミングという 3 つの異なるオプションを検討します。
.NET プログラミングでは、.NET Framework 1.0 からリフレクションが使用可能になっています。リフレクションは、制御の反転 (IoC:Inversion of Control) コンテナーのような特殊なフレームワークを動作させるために幅広く採用されています。こうしたフレームワークは、実行時に型の依存関係を解決することで機能します。そのため、コードでは、オブジェクトとその実際の動作の背後にある具象型を認識する必要なく、インターフェイスに対して操作することができます。.NET リフレクションを使用すると、間接プログラミング形式を実装できます。つまり、コードでは中間オブジェクトを操作します。すると、この中間オブジェクトが固定インターフェイスへの呼び出しをディスパッチします。呼び出すメンバーの名前を文字列として渡すため、複数の外部ソースからそのメンバーを柔軟に読み取ることができるようになります。ターゲット オブジェクトのインターフェイスは固定 (不変) です。つまり、リフレクションを介した呼び出しの背後には、常に、既知のインターフェイスが存在します。
動的型指定では、コンパイル済みのコードは、コンパイル時に検出した型の静的構造を無視します。実際に、すべての型チェックが実行時まで遅延されます。コーディングの対象となるインターフェイスは固定 (不変) ですが、使用する値から返されるインターフェイスが、状況によって異なることがあります。
.NET Framework 4 では、静的型では対応できなかった新機能がいくつか導入されています。2010 年 5 月号のコラムでは、新しい dynamic キーワードについて説明しました。今回は、expando オブジェクトや動的オブジェクトなど、動的に定義される型のサポートについて説明します。動的オブジェクトを使用すると、その型のインターフェイスをプログラムで定義することができます。一部のアセンブリに静的に格納された定義から読み取る必要はありません。動的オブジェクトにより、静的に型指定されるオブジェクトの明確さと、動的な型の柔軟性が結び付けられます。
動的オブジェクトのシナリオ
動的オブジェクトは、静的な型の長所を新たに置き換えるものではありません。静的な型は、ソフトウェア開発の基盤として、現在、そして、今後も残っていくでしょう。静的型指定では、コンパイル時に確実に型エラーを検出することにより、実行時のチェックが不要となり、処理速度が向上するコードを作成できます。さらに、コンパイルが正常に完了する必要があるため、開発者とアーキテクトは、ソフトウェアの設計や、相互作用するレイヤーのパブリック インターフェイスの定義に慎重になります。
しかし、比較的構造化されたデータ ブロックをプログラムで使用するような状況があるとします。理想的には、こうしたデータはオブジェクトを介して公開されることが望まれますが、実際には、ネットワーク接続経由で受け取る場合でも、ディスク ファイルから読み取る場合でも、データのプレーンなストリームとして受け取ります。このようなデータを操作するオプションは 2 つあります。1 つは間接的アプローチの使用、もう 1 つはアドホック型の使用です。
間接的アプローチでは、プロキシとして機能し、照会や更新を調整するジェネリック API を使用します。アドホック型を使用する場合は、操作対象のデータを完全にモデル化する特定の型を用意します。この場合、だれがこのようなアドホック型を作成するかということが問題になります。
.NET Framework の一部のセグメントには、特定のデータ ブロック用にアドホック型を作成する内部モジュールの優れた例が既にあります。わかりやすい例の 1 つは、ASP.NET Web フォームです。ASPX リソースに要求を行うと、Web サーバーが ASPX サーバー ファイルのコンテンツを取得します。次に、そのコンテンツが文字列に読み込まれ、HTML 応答に処理されます。その結果、操作に使用できる比較的構造化されたテキストになります。
このデータになんらかの処理を行うには、サーバー コントロールへの参照を把握し、その参照のインスタンスを正しく作成して、それらを 1 つのページにリンクする必要があります。これを確実に行うには、要求ごとに XML ベースのパーサーを使用することになります。ただし、その過程では、結局のところ、要求ごとにパーサーの余計なコストがかかることになります。これは、おそらく、容認できないコストです。
このようにデータの解析に追加のコストがかかるため、ASP.NET チームでは、動的にコンパイルできるクラスにマークアップを解析する 1 回限りのステップを導入することに決めました。その結果、次のような単純なマークアップが、Web フォーム ページの分離コード クラスから派生されるアドホック クラスで使用されています。
<html>
<head runat="server">
<title></title>
</head>
<body>
<form id="Form1" runat="server">
<asp:TextBox runat="server" ID="TextBox1" />
<asp:Button ID="Button1" runat="server" Text="Click" />
<hr />
<asp:Label runat="server" ID="Label1"></asp:Label>
</form>
</body>
</html>
図 1 は、マークアップから作成されたクラスのランタイム構造を示しています。灰色で表示しているメソッド名は、runat=server 要素を含む要素を、サーバー コントロールのインスタンスに解析するために使用される内部プロシージャーです。
図 1 動的に作成された Web フォーム クラスの構造
このアプローチは、アプリケーションで外部データを受け取り、繰り返し処理するほとんどの状況に適用できます。たとえば、アプリケーションに XML データのストリームが渡されるところを考えてみましょう。XML データを処理できる API は、XML DOM から LINQ-to-XML までいくつかあります。いずれの場合も、XML DOM API または LINQ-to-XML API を照会して間接的に続行するか、同じ API を使用して未加工のデータをアドホック オブジェクトに解析する必要があります。
.NET Framework 4 の動的オブジェクトでは、一部の未加工データに基づいて動的に型を作成するために、より単純な代替 API が提供されます。簡単な例として、次の XML 文字列を考えてみましょう。
<Persons>
<Person>
<FirstName> Dino </FirstName>
<LastName> Esposito </LastName>
</Person>
<Person>
<FirstName> John </FirstName>
<LastName> Smith </LastName>
</Person>
</Persons>
この XML 文字列をプログラミング可能な型に変換する場合、.NET Framework 3.5 では、おそらく、図 2 のようなコードを使用するでしょう。
図 2 LINQ-to-XML を使用した Person オブジェクトへのデータの読み込み
var persons = GetPersonsFromXml(file);
foreach(var p in persons)
Console.WriteLine(p.GetFullName());
// Load XML data and copy into a list object
var doc = XDocument.Load(@"..\..\sample.xml");
public static IList<Person> GetPersonsFromXml(String file) {
var persons = new List<Person>();
var doc = XDocument.Load(file);
var nodes = from node in doc.Root.Descendants("Person")
select node;
foreach (var n in nodes) {
var person = new Person();
foreach (var child in n.Descendants()) {
if (child.Name == "FirstName")
person.FirstName = child.Value.Trim();
else
if (child.Name == "LastName")
person.LastName = child.Value.Trim();
}
persons.Add(person);
}
return persons;
}
このコードでは、LINQ-to-XML を使用して、未加工のコンテンツを Person クラスのインスタンスに読み込みます。
public class Person {
public String FirstName { get; set; }
public String LastName { get; set; }
public String GetFullName() {
return String.Format("{0}, {1}", LastName, FirstName);
}
}
.NET Framework 4 では、同様の結果をもたらす別の API が提供されます。新しい ExpandoObject クラスを中心とするこの API は、より直接的な記述を行い、Person クラスの計画、記述、デバッグ、テスト、およびメンテナンスを必要としません。では、ExpandoObject について詳しく見てみることにしましょう。
ExpandoObject クラスの使用
Expando オブジェクトは、.NET Framework 向けに考案されたわけではありません。実際には、.NET の数年前に登場しています。最初は、1990 年代半ばに JScript オブジェクトの説明に使用されている用語として耳にしました。expando は、一種の拡張可能なオブジェクトで、その構造全体は実行時に定義されます。.NET Framework 4 では、このオブジェクトを従来のマネージ オブジェクトであるかのように使用します。ただし、マネージ オブジェクトとは異なり、その構造はアセンブリから読み取られるのではなく、すべて動的に作成されます。
expando オブジェクトは、構成ファイルのコンテンツなど、動的に変化する情報をモデル化するのに理想的です。では、ExpandoObject クラスを使用して前述の XML ドキュメントのコンテンツを格納する方法を見てみましょう。完全なソース コードを図 3 に示します。
図 3 LINQ-to-XML を使用した Expando オブジェクトへのデータの読み込み
public static IList<dynamic> GetExpandoFromXml(String file) {
var persons = new List<dynamic>();
var doc = XDocument.Load(file);
var nodes = from node in doc.Root.Descendants("Person")
select node;
foreach (var n in nodes) {
dynamic person = new ExpandoObject();
foreach (var child in n.Descendants()) {
var p = person as IDictionary<String, object>);
p[child.Name] = child.Value.Trim();
}
persons.Add(person);
}
return persons;
}
この関数では、動的に定義されるオブジェクトのリストを返します。LINQ-to-XML を使用して、マークアップ内のノードを解析し、ノードごとに ExpandoObject インスタンスを作成します。<Person> の下の各ノードの名前は、expando オブジェクトの新しいプロパティになります。プロパティの値は、ノードの内部テキストです。上記の XML コンテンツに基づくと、最終的には、FirstName プロパティが Dino に設定された expando オブジェクトになります。
ただし、図 3 では、expando オブジェクトの設定にインデクサーの構文が使用されています。これについてはもう少し説明が必要です。
ExpandoObject クラスの内部
ExpandoObject クラスは、System.Dynamic 名前空間に属しており、System.Core アセンブリで定義されています。ExpandoObject は、実行時にメンバーを動的に追加および削除できるオブジェクトを表します。このクラスは、シールされていて、多数のインターフェイスを実装します。
public sealed class ExpandoObject :
IDynamicMetaObjectProvider,
IDictionary<string, object>,
ICollection<KeyValuePair<string, object>>,
IEnumerable<KeyValuePair<string, object>>,
IEnumerable,
INotifyPropertyChanged;
ご覧のとおり、このクラスは、IDictionary<String, Object> や IEnumerable など、さまざまな列挙可能なインターフェイスを使用してそのコンテンツを公開します。さらに、IDynamicMetaObjectProvider も実装します。これは標準インターフェイスで、これを使用すると、動的言語ランタイム (DLR) の相互運用性のモデルに従って作成されたプログラムでは、DLR 内でオブジェクトを共有できるようになります。つまり、.NET の動的言語で共有できるのは、IDynamicMetaObjectProvider インターフェイスを実装するオブジェクトのみです。expando オブジェクトは、いわゆる IronRuby コンポーネントに渡すことができます。この処理は、通常の .NET マネージ オブジェクトでは簡単に行うことができません。また、できたとしても、動的な動作にはなりません。
ExpandoObject クラスは、INotifyPropertyChanged インターフェイスも実装します。これにより、クラスは、メンバーが追加または変更されたときに PropertyChanged イベントを発生させることができます。INotifyPropertyChanged インターフェイスのサポートは、Silverlight と Windows Presentation Foundation のアプリケーション フロントエンドで expando オブジェクトを使用する際に重要です。
ExpandoObject インスタンスを格納する変数の型が動的である点を除き、他の .NET オブジェクトと同様に、ExpandoObject インスタンスを作成します。
dynamic expando = new ExpandoObject();
この時点で、expando にプロパティを追加するには、次のように、プロパティに新しい値を代入するだけです。
expando.FirstName = "Dino";
FirstName メンバー、その型、またはその可視性に関する情報がなくても問題ありません。これは動的なコードです。したがって、var キーワードを使用して ExpandoObject インスタンスを変数に代入する場合は、大きな違いが生じます。
var expando = new ExpandoObject();
このコードは正常にコンパイルされ、機能します。ただし、この定義では、FirstName プロパティに値を代入することはできません。System.Core で定義された ExpandoObject クラスにはこのようなメンバーはありません。もっと正確に言えば、ExpandoObject クラスにはパブリック メンバーはありません。
これは重要なポイントです。expando の静的型が dynamic のときは、操作が、メンバーの照合など、動的な操作としてバインドされます。静的型が ExpandoObject のときは、操作が、通常のコンパイル時のメンバー照合としてバインドされます。そのため、コンパイラは dynamic が特殊な型であると認識しますが、ExpandoObject が特殊な型であるとは認識しません。
図 4 では、expando オブジェクトが dynamic 型として宣言されている場合やこのオブジェクトがプレーンな .NET オブジェクトとして処理される場合の Visual Studio 2010 IntelliSense オプションを示しています。後者の場合は、IntelliSense により、既定の System.Object メンバーに加え、コレクション クラスの拡張メソッドの一覧が表示されます。
図 4 Visual Studio 2010 IntelliSense と Expando オブジェクト
また、状況によっては、一部の市販ツールがこの基本動作以外の処理も行う場合があることにも注意してください。図 5 は、現在オブジェクトで定義されているメンバーの一覧をキャプチャする ReSharper 5.0 を示しています。これは、プログラムによってインデクサーからメンバーが追加される場合には行われません。
図 5 Expando オブジェクトを操作する際の ReSharper 5.0 IntelliSense
expando オブジェクトにメソッドを追加するには、そのメソッドをプロパティとして定義するだけです。ただし、Action<T> デリゲートまたは Func<T> デリゲートを使用して動作を表す点は除きます。その例を次に示します。
person.GetFullName = (Func<String>)(() => {
return String.Format("{0}, {1}",
person.LastName, person.FirstName);
});
GetFullName メソッドによって返される文字列は、expando オブジェクトで使用可能と考えられる姓と名のプロパティを組み合わせて取得されます。expando オブジェクトに存在しないメンバーへのアクセスを試みると、RuntimeBinderException 例外が返されます。
XML 駆動型のプログラム
ここまで説明してきた概念をまとめるため、データの構造と UI の構造が XML ファイルで定義されている例を紹介します。ファイルのコンテンツは、expando オブジェクトのコレクションに解析され、アプリケーションによって処理されます。ただし、アプリケーションは、動的に表示される情報のみを操作し、静的な型にはバインドされません。
図 3 のコードでは、動的に定義される person expando オブジェクトの一覧を定義しています。ご想像どおり、XML スキーマに新しいノードを追加すると、expando オブジェクトに新しいプロパティが作成されます。外部ソースからメンバーの名前を読み取る必要がある場合は、インデクサー API を使用してその名前を expando に追加します。ExpandoObject クラスは、IDictionary<String, Object> インターフェイスを明示的に実装します。つまり、インデクサー API または Add メソッドを使用するには、dictionary 型から ExpandoObject インターフェイスを分離する必要があります。
(person as IDictionary<String, Object>)[child.Name] = child.Value;
この動作が原因で、別のデータ セットを使用できるように XML ファイルを編集する必要がありますが、この動的に変化するデータはどのように使用すればよいのでしょう。変化する一連のデータを受け取れるように UI の柔軟性を高める必要があります。
コンソールにデータを表示するだけという簡単な例を考えてみましょう。ユーザーのコンテキストで何を意味しているかにかかわらず、必要な UI が記述されたセクションが XML ファイルに含まれているとします。たとえば、次のようなセクションがあります。
<Settings>
<Output Format="{0}, {1}"
Params="LastName,FirstName" />
</Settings>
この情報は、次のコードを使用して、別の expando オブジェクトに読み込まれます。
dynamic settings = new ExpandoObject();
settings.Format =
node.Attribute("Format").Value;
settings.Parameters =
node.Attribute("Params").Value;
メイン プロシージャの構造は次のとおりです。
public static void Run(String file) {
dynamic settings = GetExpandoSettings(file);
dynamic persons = GetExpandoFromXml(file);
foreach (var p in persons) {
var memberNames =
(settings.Parameters as String).
Split(',');
var realValues =
GetValuesFromExpandoObject(p,
memberNames);
Console.WriteLine(settings.Format,
realValues);
}
}
expando オブジェクトには、出力の書式と、表示する値を持つメンバーの名前が含まれています。person 動的オブジェクトの場合は、次のようなコードを使用して、指定されたメンバーの値を読み込む必要があります。
public static Object[] GetValuesFromExpandoObject(
IDictionary<String, Object> person,
String[] memberNames) {
var realValues = new List<Object>();
foreach (var m in memberNames)
realValues.Add(person[m]);
return realValues.ToArray();
}
expando オブジェクトは IDictionary<String, Object> を実装するため、インデクサー API を使用して値を取得および設定できます。
最後に、expando オブジェクトから取得された値の一覧がコンソールに渡され、実際に表示されます。図 6 では、サンプル コンソール アプリケーションの画面を 2 とおり示しています。この違いは、基になる XML ファイルの構造だけです。
図 6 XML ファイルによって動作する 2 つのサンプル コンソール アプリケーション
確かに、これはつまらない例ですが、もっと興味深い例にしても、動作させるために必要なメカニズムは同じです。ぜひお試しのうえ、フィードバックをお寄せください。
Dino Esposito は、『Programming ASP.NET MVC』(Microsoft Press) の著者であり、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2008 年) の共著者でもあります。Esposito はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos (英語) です。
この記事のレビューに協力してくれた技術スタッフの Eric Lippert に心より感謝いたします。