DynamicXml:XMLデータを操作するための「動的」シェル

静的型付けが大好きであるにもかかわらず、いくつかのシナリオでは、動的型付けがもたらす自由の利点が、それに関連する欠点を上回る可能性があることをすでに書きました。 前回はDynamic LINQについて説明しましたが、今回はdynamicと呼ばれる新しいC#4.0機能を使用して、XMLなどの弱い型指定のデータを操作することについて説明します。





この記事で説明したDynamicXmlライブラリのソースコードは、 githubで入手できます。



はじめに



バージョン4.0以降、C#はdynamicという新しい静的型のおかげで、動的プログラミングのサポートを導入しました。 本質的に、このキーワードの使用は、コンパイル時にこれらすべての特性を定義する代わりに、実行時にバインディングおよびディスパッチ操作が実行されるように、必要なすべてのコードを生成するようコンパイラーに指示します。 この場合、コンパイラーはDLRライブラリを使用して必要なコードをすべて生成します-Dynamic Language Runtime(*)。これはもともとプログラミング言語Iron Pythonの設計時に作成され、その後動的プログラミング言語の実装の基礎として.Net Framework 4.0の一部になりました。それらの間の相互作用。





動的キーワードの出現にもかかわらず、C#プログラミング言語は基本的に静的に型付けされたままでした。 このコードで実際に何が起こるかについての決定は、実行時まで遅れることを明示的に示す必要があります。 さらに、この機会を毎日使用することを提案する人はいません。 この関数は、主にIron Python、Iron Rubyなどの他の動的に型付けされた言語とのやり取り、およびVSTO(Visual Studio Tools for Office)や他のCOM APIなどの弱い型付けされた環境とのやり取りを目的としています。 ダイナミックを使用するもう1つの典型的な例は、オブジェクトの「ダイナミック」ラッパーを作成することです。 非常によく知られた例は、プライベートまたは保護されたクラスメンバー(**)にアクセスするためのラッパーの作成です。 同様によく知られているもう1つの例は、XMLデータにアクセスするための動的ラッパーの作成です。 ここでやめるのは、まさに第2の可能性の実装です。



XMLデータを読み取る簡単な例



したがって、次のデータ(***)を含む行があると仮定します。



< books >

< book >

< title > Mortal Engines </ title >

< author name ="" Philip Reeve "" />

</ book >

< book >

< title > The Talisman </ title >

< author name ="" Stephen King "" />

< author name ="" Peter Straub "" />

</ book >

</ books >




* This source code was highlighted with Source Code Highlighter .








そして、私たちの仕事は、このデータを読み取って処理する簡単なコードを書くことです。 もちろん、場合によっては、 XmlSerializerクラスを使用してこのすべてをビジネスロジックオブジェクト(この場合はBook型のエンティティのリスト)に逆シリアル化し、このビジネスオブジェクトを既に操作する方が合理的ですが、多くの場合、より軽いソリューションの方がはるかに優れています。たとえば、LINQ 2 XMLに基づいています。



上記の行がbooksという変数に含まれると仮定すると、非常に簡単なコードを使用して名前を取得し、データを取得できます。



var element = XElement .Parse(books);

string firstBooksTitle =

element.Element( "book" ).Element( "title" ).Value;

Assert.That(firstBooksTitle, Is.EqualTo( "Mortal Engines" ));



string firstBooksAuthor =

element.Element( "book" ).Element( "author" ). Attribute ( "name" ).Value;

Assert.That(firstBooksAuthor, Is.EqualTo( "Philip Reeve" ));



string secondBooksTitle =

element.Elements().ElementAt(1).Element( "title" ).Value;

Assert.That(secondBooksTitle, Is.EqualTo( "The Talisman" ));




* This source code was highlighted with Source Code Highlighter .








さらに、 XElementの明示的な使用に対して絶対に何もありません。さらに、このオプションは非常にシンプルでエレガントですが、それでもこのコードには欠点がないわけではありません。 第一に、それは非常に冗長であり、第二に、エラー処理に関して完全に正直ではありません: books変数にbookという名前の要素またはtitleという名前の要素がない場合、 NullReferenceExceptionを受け取ります。 そのため、このコードはファイルを使用してファイナライズする必要があり、その読み取り、理解、およびメンテナンスが多少複雑になります。



// Dynamic Wrapper XElement

dynamic dynamicElement = // ...



//

string firstBooksTitle = dynamicElement.book.title;

Assert.That(firstBooksTitle, Is.EqualTo( "Mortal Engines" ));



// , ,

string firstBooksAuthor = dynamicElement.book.author[ "name" ];

Assert.That(firstBooksAuthor, Is.EqualTo( "Philip Reeve" ));



// , ,

string secondBooksTitle = dynamicElement.book[1].title;

Assert.That(secondBooksTitle, Is.EqualTo( "The Talisman" ));




* This source code was highlighted with Source Code Highlighter .








要素へのアクセスと属性へのアクセスを分離する必要があるため、インデクサーを使用して属性値にアクセスする必要がありますが、後で見るように、すべてが完全に手にあるため、別の決定を下し、属性へのアクセスを実装できますその他の構文。 それでも、得られた構文は、LINQ 2 XMLを直接使用するコードよりも単純で理解しやすいものです。1つの簡単な質問に答える必要があります。XElementオブジェクトの動的ラッパー可能です。



XMLデータを読み取るための「動的」ラッパーの作成





動的シェルを作成する最も簡単な方法は、同時にかなり幅広い機能を備えていますが、 System.Dynamic名前空間のDynamicObjectクラスを使用することです 。 このクラスには、 TryXXX型の複数の仮想関数が含まれています。これにより、メソッド呼び出し、プロパティへのアクセス、型変換など、実行時に発生するすべての基本アクションを動的オブジェクトで「インターセプト」できます。



したがって、 DynamicObjectから継承者を作成するだけで、 XElementオブジェクトをコンストラクターパラメーターとして受け取り、いくつかのヘルパーメソッドをオーバーライドできます。



/// <summary>

/// " " XElement

/// </summary>

public class DynamicXElementReader : DynamicObject

{

private readonly XElement element;



private DynamicXElementReader( XElement element)

{

this .element = element;

}



public static dynamic CreateInstance( XElement element)

{

Contract.Requires(element != null );

Contract.Ensures(Contract.Result< object >() != null );



return new DynamicXElementReader(element);

}

}




* This source code was highlighted with Source Code Highlighter .








この場合のファクトリメソッドの使用は、このクラスの使用のコンテキストをより明確に示しているためです。 このメソッドに加えて、DynamicXmlライブラリコードには、動的シェルのインスタンスをより便利に作成できる拡張メソッドを備えた静的クラスも含まれています。 この場合、コントラクト(コードコントラクトライブラリ)を使用すると、そのようなライブラリクラスの作成が簡単になり、テストとドキュメントが簡単になり、静的アナライザーを使用してコンパイル中にエラーを見つけることができます。 これは私の個人的な好みですが、このアプローチがあなたに同情的であると思われる場合(非常に無駄ですが)、マジック検索/置換ツールを使用して、入力パラメータをチェックするための便利なメカニズムでコントラクトを置き換えることができます。



それでは、 DynamicXElementReaderクラスの実装に戻りましょう。 最初に、ちょっとした理論: DynamicObjectから相続人クラスのプロパティまたはメソッドへの呼び出しは2段階で行われます:最初に、この同じ相続人の同じ名前を持つ対応するメソッドまたはプロパティが検索され、次に対応するメソッドが呼び出され、 そこでこのメンバーの不在を動的に処理できます。 ラッパーは考えられることも考えられないことも絶対に提供しないので(そしてほとんどの場合これは必要ありません)、基礎となるXElementがラッパーから取得されることを保証する必要があります。 さらに、前の例で見たように、2つのインデクサーを作成する必要があります。1つはintを取り、サブアイテムを返す必要があり、2つ目は文字列(または、後で見るようにXName )を取り属性を返す必要があります。



public class DynamicXElementReader : DynamicObject

{

/// <summary>

/// true, .

/// </summary>

/// <remarks>

/// Pure

/// </remarks>

[Pure]

public bool HasParent()

{

return element.Parent != null ;

}



public dynamic this [XName name]

{

get

{

Contract.Requires(name != null );



XAttribute attribute = element. Attribute (name);



if (attribute == null )

throw new InvalidOperationException(

"Attribute not found. Name: " + name.LocalName);



return attribute.AsDynamic();

}

}



public dynamic this [ int idx]

{

get

{



Contract.Requires(idx >= 0, "Index should be greater or equals to 0" );

Contract.Requires(idx == 0 || HasParent(),

"For non-zero index we should have parent element" );



//

if (idx == 0)

return this ;



// "" .

// ,

var parent = element.Parent;

Contract.Assume(parent != null );



XElement subElement = parent.Elements().ElementAt(idx);



// subElement null, ElementAt

// , .

// ,

// Contract.Assume

Contract.Assume(subElement != null );



return CreateInstance(subElement);

}

}



public XElement XElement { get { return element; } }



}




* This source code was highlighted with Source Code Highlighter .








最初のインデクサーはXNameをパラメーターとして使用し、現在の要素の属性をその名前で取得するように設計されています。 戻り値の型も動的であり、実際の戻り値はXAttributeオブジェクトのAsDynamic拡張メソッドを呼び出すことで取得されます。 原則として、戻り値の型としてXAttribute型を使用するユーザーはいませんが、この場合、属性の直接値を取得するには、受信した値のValueプロパティに追加でアクセスするか、明示的な型キャストを使用する必要があります。 一般に、属性の動的シェルの実装ははるかに単純で、同様の方法で実装されます。



次に、 DynamicObjectクラスの2つのメイン(このクラス用)仮想メソッドの実装に移ります。TryGetMemberメソッド-dynamicObject.Member型のプロパティまたはフィールドへのアクセスを担当し、 TryConvertメソッド-型が動的型付きオブジェクトから静的型に暗黙的に変換されるときに呼び出されます型付き、 文字列値= dynamicObject



public class DynamicXElementReader : DynamicObject

{

// not used

private XElement element;

public static dynamic CreateInstance( XElement ) { return null ;}



/// <summary>

/// :

/// SomeType variable = dynamicElement;

/// </summary>

public override sealed bool TryConvert(ConvertBinder binder, out object result)

{

// XElement

// xml-

if (binder.ReturnType == typeof ( XElement ))

{

result = element;

return true ;

}



//

//

string underlyingValue = element.Value;

result = Convert .ChangeType(underlyingValue, binder.ReturnType,

CultureInfo.InvariantCulture);



return true ;

}



/// <summary>

///

/// </summary>

public override bool TryGetMember(GetMemberBinder binder, out object result)

{

string binderName = binder.Name;

Contract.Assume(binderName != null );



// ,

//

XElement subelement = element.Element(binderName);

if (subelement != null )

{

result = CreateInstance(subelement);

return true ;

}



// ,

//

return base .TryGetMember(binder, out result);

}



}




* This source code was highlighted with Source Code Highlighter .








上記のように、xml要素またはそのサブ要素の1つを指定された型に変換しようとすると、 TryConvertメソッド呼び出されます。 現在のxml要素の値を簡単に取得できるため、このメソッドを実装するために必要なのは、 ConvertクラスのChangeTypeを呼び出すことだけです。 唯一の例外はXElementタイプです。これは個別に処理され、基になるXElementを直接取得できます。



TryGetMemberメソッドも非常に簡単です。最初に、ユーザーコードがアクセスしようとしているメンバーの名前を取得してから、この名前の要素を見つけようとします。 指定された要素が見つかった場合、動的シェルを作成し、出力パラメーターの結果を通じてそれを返します。 それ以外の場合は、ベースバージョンを呼び出します。これにより、要求されたメンバーが見つからなかったというランタイム例外が発生します。



これにより、次のようにシェルを使用できます。



// bookXElement

string firstBooksElement = dynamicElement.book;

Console .WriteLine( "First books element: {0}" , firstBooksElement);



//

string firstBooksTitle = dynamicElement.book.title;

Console .WriteLine( "First books title: {0}" , firstBooksTitle);



// int

int firstBooksPageCount = dynamicElement.book.pages;

Console .WriteLine( "First books page count: {0}" , firstBooksPageCount);




* This source code was highlighted with Source Code Highlighter .








このコードを実行した結果:



First books element: < book >

< title > Mortal Engines </ title >

< author name ="Philip Reeve" />

< pages > 347 </ pages >

</ book >



First books title: Mortal Engines

First books page count: 347

First books author: Philip Reeve

Second books title: The Talisman




* This source code was highlighted with Source Code Highlighter .








XMLデータを作成/変更するための「動的」シェルの作成



データを作成するクラスと作成および変更するクラスの2つのクラスを作成する理由は、 TryGetMemberメソッドの実装では、基礎となるメンバーにアクセスする理由を事前に知ることができないという事実によるものです。 結局、この呼び出しがデータを読み取るために発生し、指定された要素がソースXMLデータにない場合、最も論理的な動作は、指定された名前の要素が見つからなかったという例外をスローすることです。 これが、 DynamicXElementReaderクラスの上記の実装の動作です。 ただし、XMLデータを作成/変更するときは、まったく異なる動作が必要です。この場合、例外をスローする代わりに、指定された名前で空の要素を作成する必要があります。 作成された要素には、指定された名前の要素が存在しない可能性がある(または、そうではない可能性が高い)と想定するのは非常に論理的です。



したがって、上記の読み取り専用DynamicXElementReaderクラスに、別のクラス-DynamicXElementWriterを追加します。そのタスクは、XMLデータを作成および変更することです。 ただし、2つのクラスには多くの共通点があるため、たとえば、 TryConvertメソッドの実装とHasParentなどの一部のヘルパーメソッドのため、実際のコードには別のヘルパークラスDynamixXElementBaseが含まれており、コードの重複を排除し、その子孫の実装を簡素化します。 ただし、追加の基本クラスを使用してコードを分析することはやや複雑なので、ここでは示しません。



XMLデータの作成/変更を目的とした動的シェルの主な違いは、2つのインデクサーのセッターが存在することです。1つは属性値を変更し、もう1つは要素を追加します。 2番目の違いは、現在の要素とその属性の値を変更するために使用されるSetValueSetAttributeValueの2つの追加の非動的メソッドの存在です。



public class DynamicXElementWriter : DynamicObject

{

// ,



/// <summary>

///

/// </summary>

public void SetValue( object value )

{

Contract.Requires( value != null );



element.SetValue( value );

}



/// <summary>

///

/// </summary>

public void SetAttributeValue(XName name, object value )

{

Contract.Requires(name != null );

Contract.Requires( value != null );



element.SetAttributeValue(name, value );

}



/// <summary>

///

/// </summary>

public dynamic this [XName name]

{

get

{

//

}



set

{

//

// XElement.SetAttributeValue,

element.SetAttributeValue(name, value );

}



}



/// <summary>

/// ""

/// </summary>

public dynamic this [ int idx]

{

get

{

//

Contract.Requires(idx >= 0, "Index should be greater or equals to 0" );

Contract.Requires(idx == 0 || HasParent(),

"For non-zero index we should have parent element" );



//

if (idx == 0)

return this ;



// "" .

// ,

var parent = element.Parent;

Contract.Assume(parent != null );



// "" ,

//

XElement subElement = parent.Elements(element.Name).ElementAtOrDefault(idx);

if (subElement == null )

{

XElement sibling = parent.Elements(element.Name).First();

subElement = new XElement (sibling.Name);

parent.Add(subElement);

}



return CreateInstance(subElement);

}



set

{

Contract.Requires(idx >= 0, "Index should be greater or equals to 0" );

Contract.Requires(idx == 0 || HasParent(),

"For non-zero index we should have parent element" );



//

// ,

//

dynamic d = this [idx];

d.SetValue( value );

return ;

}



}

}




* This source code was highlighted with Source Code Highlighter .








ゲッターの実装は、特にXNameを受け入れ、属性を処理するように設計されたインデクサーの場合、以前の実装と非常に似ています。 整数をとるインデクサーの実装はやや複雑です。これは、そのような要素がない場合、ゲッターに追加の「兄弟」を作成するための追加ロジックが含まれているためです。 どちらの場合でもセッターの実装は非常に簡単です。



もう1つの重要な違いは、 TryGetMemberメソッドの実装と、要素のxml値が設定されたときに呼び出される追加のTrySetMemberメソッドの存在です: dynamicElement.SubElement = value



/// <summary>

///

/// </summary>

public override bool TryGetMember(GetMemberBinder binder, out object result)

{

string binderName = binder.Name;

Contract.Assume(binderName != null );



//

XElement subelement = element.Element(binderName);



// ,

//

if (subelement == null )

{

subelement = new XElement (binderName);

element.Add(subelement);

}



result = CreateInstance(subelement);

return true ;

}



/// <summary>

///

/// </summary>

public override bool TrySetMember(SetMemberBinder binder, object value )

{

Contract.Assume(binder != null );

Contract.Assume(! string .IsNullOrEmpty(binder.Name));

Contract.Assume( value != null );



string binderName = binder.Name;



// ,

// , ;

// XElement.SetElementValue,

//

if (binderName == element.Name)

element.SetValue( value );

else

element.SetElementValue(binderName, value );

return true ;

}




* This source code was highlighted with Source Code Highlighter .








TryGetValueメソッドの実装の主な違いは、元のxmlツリーにないサブ要素にアクセスすると、例外をスローする代わりに指定された名前の要素が追加されることです。 TrySetMemberメソッドの実装も、 XElement.SetElementValueメソッドがすべてのダーティな作業を行い、必要に応じて目的の名前の要素を追加するため、それほど複雑でありません。



結論



上記の実装にエラーが含まれていることや、この質問またはその質問で完全ではないことを排除しません。 ただし、この記事の主な目的は、静的に型指定されたオブジェクトの周囲に動的シェルを作成する原理を示すことと、C#などの静的に型指定されたプログラミング言語での動的プログラミングの利点を示すことです。 そして、与えられた実装は理想からはほど遠いかもしれませんが、非常によくテストされており、いくつかの小さなプロジェクトに成功裏に参加しています。 さらに、 githubで自由に利用でき、各自の判断(および実装)を自由に使用できます。



繰り返しになりますが、DynamicXmlライブラリのソースコードはこちらから入手できます



-(*)最も興味深いのは、DLR-動的言語ランタイムはランタイムとは何の関係もありませんが、式ツリーを巧みに操作する「通常の」ライブラリにすぎないということです。



(**)この可能性を示すいくつかの例があります 。たとえば、 hereおよびhereです。



(***)これは、John Skeetが彼の著書「C#In Depth」の第2版の例で使用したわずかに変更された例です。



All Articles