dynamicを使用したC#4.0での動的式の計算

こんにちは。

昨日、C#4.0の4番目のバージョンの新しい機能の1つである動的キーワードに関する記事の翻訳を Habréに公開しました。 コメントでは、白​​熱した議論が始まりました。その主な動機は、スピーカーのパフォーマンスとスコープの2つでした。 この記事では、最初の質問については触れませんが、新しい機会を利用して、ほんの数時間で最小限の労力で非常に現実的な問題を解決できる方法の例を示します。





背景



問題をよりよく理解するために、私と同僚が2年前に取り組んだ最初の商業プロジェクトについて簡単に説明します。 C#2.0でオブジェクトの一意の分類子を実装するタスクに直面しました。これは、特定の文書(納税申告書など)が特定の基準を満たすかどうかの質問に答えました。 基準は、これらの文書の処理を担当する公務員によって形成された複雑な述語であり、サービスが細心の注意を払うべき文書の流れの中のそれらを識別するのを助けました。 まあ、なぜ私はあなたに教えないのに注意する必要がありますか?データベースから基準を満たすオブジェクトを選択できるクエリ。 最初のモードはその後システムから削除されましたが、私はそれを実装するのがどれほど難しいか、それと同時に面白いことを今でも覚えています。 その後、すべてのことをすべて行うのに数ヶ月かかりました。 C#4.0の新機能のおかげで、わずか数時間で、オブジェクトの同様の(もちろん、はるかに原始的な)分類子を再作成することができました。



実装



それでは始めましょう。 ご存じのように、式は基本的にツリーであり、そのノードは定数、変数、演算子、および関数です。 分類子は、オブジェクトを受け取り、オブジェクトがこのクラスに属しているかどうかを答えるようなツリーになります。

最初に、式ツリーの最上部の基本クラスを選択します。

public abstract class ExpressionNode

{

public abstract dynamic Evaluate();



public virtual void HandleObject( object obj)

{

}



public abstract IEnumerable <ExpressionNode> GetChildren();

}




* This source code was highlighted with Source Code Highlighter .






Evaluateメソッドは、指定された頂点と、それが頂点であるサブツリー全体の値を計算します。 このメソッドによって返される値は動的型であることに注意してください。 HandleObjectを使用すると、必要なデータを記憶するために、計算の前にオブジェクトを処理できます。 GetChildrenは、ツリーをナビゲートするために使用されます。

定数のクラス:

public class Constant : ExpressionNode

{

private readonly dynamic _value;



public Constant(dynamic value )

{

_value = value ;

}



public override dynamic Evaluate()

{

return _value;

}



public override IEnumerable <ExpressionNode> GetChildren()

{

yield break ;

}

}





* This source code was highlighted with Source Code Highlighter .






定数は、値を保存し、必要に応じて返す式の非常に単純な頂点です。

処理されたオブジェクト(式の変数)のプロパティへのアクセスを提供するクラス:

public class ObjectProperty : ExpressionNode

{

private dynamic _value = null ;

private readonly string _name;

private bool _propertyNotFound;



public ObjectProperty( string name)

{

_name = name;

}



public override void HandleObject( object obj)

{

try

{

_value = obj.GetType().GetProperty(_name).GetValue(obj, new object [0]);

}

catch (Exception ex)

{

_propertyNotFound = true ;

_value = null ;

}

}



public override dynamic Evaluate()

{

if (_propertyNotFound)

{

throw new PropertyNotFoundException();

}

return _value;

}



public override IEnumerable <ExpressionNode> GetChildren()

{

yield break ;

}

}



* This source code was highlighted with Source Code Highlighter .






定数に非常に似ていますが、値は処理中に新しいオブジェクトごとに計算されます。 オブジェクトにプロパティがない場合、例外は処理の段階ではなく計算の段階でのみスローされることに注意してください。 この機能により、遅延計算で複雑な式を作成できます。

オブジェクトにプロパティが存在するかどうかを確認するには、次の関数を使用します。

public class HasPropertyChecker : ExpressionNode

{

private readonly string _name;

private bool _result;



public HasPropertyChecker( string name)

{

_name = name;

}



public override dynamic Evaluate()

{

return _result;

}



public override IEnumerable <ExpressionNode> GetChildren()

{

yield break ;

}



public override void HandleObject( object obj)

{

_result = obj.GetType().GetProperty(_name) != null ;

}

}



* This source code was highlighted with Source Code Highlighter .






式の残りの頂点-演算子には、単項式、バイナリー、アグリゲーターの3つのタイプがあります。

単項の基本クラス:

public abstract class UnaryOperator : ExpressionNode

{

private readonly ExpressionNode _argument;

protected UnaryOperator(ExpressionNode arg)

{

_argument = arg;

}



protected abstract dynamic EvaluateUnary(dynamic arg);



public override dynamic Evaluate()

{

return EvaluateUnary(_argument.Evaluate());

}



public override IEnumerable <ExpressionNode> GetChildren()

{

yield return _argument;

}

}



* This source code was highlighted with Source Code Highlighter .






そして、そのような演算子の最も単純な例は論理否定です:

[Operator( "not" )]

public class Not : UnaryOperator

{

public Not( params ExpressionNode[] args) : this (args[0]) { Debug.Assert(args.Length == 1); }

public Not(ExpressionNode arg) : base (arg) { }



protected override dynamic EvaluateUnary(dynamic arg)

{

return !arg;

}

}




* This source code was highlighted with Source Code Highlighter .






演算子は、その名前を定義する特別な属性でマークされていることに注意してください。 後でXMLに基づいてツリーを形成するのに役立ちます。

基本的な二項演算子とその例である加算演算子:

public abstract class BinaryOperator : ExpressionNode

{

private readonly ExpressionNode _argument1;

private readonly ExpressionNode _argument2;



protected BinaryOperator(ExpressionNode arg1, ExpressionNode arg2)

{

_argument1 = arg1;

_argument2 = arg2;

}



protected abstract dynamic EvaluateBinary(dynamic arg1, dynamic arg2);



public override dynamic Evaluate()

{

return EvaluateBinary(_argument1.Evaluate(), _argument2.Evaluate());

}



public override IEnumerable <ExpressionNode> GetChildren()

{

yield return _argument1;

yield return _argument2;

}

}



[Operator( "add" )]

public class Add : BinaryOperator

{

public Add( params ExpressionNode[] args) : this (args[0], args[1]) { Debug.Assert(args.Length == 2); }

public Add(ExpressionNode arg1, ExpressionNode arg2) : base (arg1, arg2) { }



protected override dynamic EvaluateBinary(dynamic arg1, dynamic arg2)

{

return arg1 + arg2;

}

}



* This source code was highlighted with Source Code Highlighter .






そして最後に、基本的な集約演算子とその例、論理的な「AND」:

public abstract class AgregateOperator : ExpressionNode

{

private readonly ExpressionNode[] _arguments;



protected AgregateOperator( params ExpressionNode[] args)

{

_arguments = args;

}



protected abstract dynamic EvaluateAgregate( IEnumerable <dynamic> args);



public override dynamic Evaluate()

{

return EvaluateAgregate(_arguments.ConvertAll(x => x.Evaluate()));

}



public override IEnumerable <ExpressionNode> GetChildren()

{

return _arguments;

}

}



[Operator( "and" )]

public class And : AgregateOperator

{

public And( params ExpressionNode[] args) : base (args) { }



protected override dynamic EvaluateAgregate( IEnumerable <dynamic> args)

{

foreach (dynamic arg in args)

{

if (!arg)

{

return arg;

}

}

return true ;

}

}



* This source code was highlighted with Source Code Highlighter .






ツリー全体(ルート頂点への参照の形式)は、ラッパークラスに格納されます。

public class BooleanExpressionTree

{

public BooleanExpressionTree(ExpressionNode root)

{

Root = root;

}



public ExpressionNode Root { get ; private set ; }



public bool EvaluateOnObject(dynamic obj)

{

lock ( this )

{

PrepareTree(obj, Root);

return ( bool )Root.Evaluate();

}

}



private void PrepareTree(dynamic obj, ExpressionNode node)

{

node.HandleObject(obj);

foreach (ExpressionNode child in node.GetChildren())

{

PrepareTree(obj, child);

}

}

}



* This source code was highlighted with Source Code Highlighter .






式自体はXMLとして保存され、実行時に生成されます。 これがどのように起こるか気にする人は、あなたはソースを見ることができます-すべてがそこに些細です。



テストオブジェクト



そのため、テストのために、いくつかの多彩なクラスを用意しました(はい、それらは奇妙ですが、私を信じて、それは納税申告よりも興味深いです):

public class Human

{

public object Legs { get { return new object (); } }

public object Hands { get { return new object (); } }

public int LegsCount { get { return 2; } }

public int HandsCount { get { return 2; } }

public string Name { get ; set ; }

}



public class OldMan : Human

{

public DateTime BirthDate { get { return new DateTime (1933, 1, 1); } }

}



public class Baby : Human

{

public DateTime BirthDate { get { return new DateTime (2009, 1, 1); } }

}



public class Animal

{

public int LegsCount { get { return 4; } }

public object Tail { get { return new object (); } }

public string Name { get ; set ; }

}



public class Dog : Animal

{

public DateTime BirthDate { get ; set ; }

}



* This source code was highlighted with Source Code Highlighter .






テストオブジェクトのインスタンスを作成します。

static IEnumerable < object > PrepareTestObjects()

{

List < object > objects = new List < object >();

objects.Add( new Human { Name = "Some Stranger" });

objects.Add( new OldMan { Name = "Ivan Petrov" });

objects.Add( new OldMan { Name = "John Smith" });

objects.Add( new Baby { Name = "Vasya Pupkin" });

objects.Add( new Baby { Name = "Bart Simpson" });

objects.Add( new Dog { Name = "Sharik" , BirthDate = new DateTime (2004, 11, 11) });

objects.Add( new Dog { Name = "Old Zhuchka" , BirthDate = new DateTime (1900, 11, 11) });

return objects;

}



* This source code was highlighted with Source Code Highlighter .








表現



テストで使用される式の例:

IsHuman:

< and >

< has-property name ="Hands" />

< has-property name ="Legs" />

< not >

< has-property name ="Tail" />

</ not >

< equals >

< property name ="HandsCount" />

< constant value ="2" type ="int" />

</ equals >

< equals >

< property name ="LegsCount" />

< constant value ="2" type ="int" />

</ equals >

</ and >

IsNotAmerican:

< and >

< has-property name ="Name" />

< or >

< like >

< property name ="Name" />

< constant value ="Ivan" type ="string" />

</ like >

< like >

< property name ="Name" />

< constant value ="Vasya" type ="string" />

</ like >

</ or >

</ and >

IsOld:

< and >

< has-property name ="BirthDate" />

< gt >

< sub >

< constant value ="2009-12-23" type ="datetime" />

< property name ="BirthDate" />

</ sub >

< constant value ="22000.00:00:00" type ="timespan" />

</ gt >

</ and >



* This source code was highlighted with Source Code Highlighter .






XMLの外観から、これらの式がチェックされていることは明らかだと思います(これらは、過度に複雑な読者を退屈させないように、オブジェクト自体のように意図的にプリミティブです)。 これらの式が生成されたオブジェクトを分類する方法は次のとおりです。

テストは人間です:

Some Stranger-True

イワン・ペトロフ-True

ジョン・スミス-True

ヴァシャ・パプキン-True

バートシンプソン-True

シャリック-偽

Old Zhuchka-偽

テストは古い:

Some Stranger-False

イワン・ペトロフ-True

ジョン・スミス-True

ヴァシャ・パプキン-偽

バートシンプソン-偽

シャリック-偽

Old Zhuchka-True

テストはアメリカではありません:

Some Stranger-False

イワン・ペトロフ-True

ジョン・スミス-偽

ヴァシャ・パプキン-True

バートシンプソン-偽

シャリック-偽

Old Zhuchka-偽





結論



スピーカーは、C#の完全に新しい機能を導入しました。これにより、最小限のコードを使用して、動的式の計算など、多くの重要なタスクを迅速に実装できます。 多くの場合、この機能をパフォーマンスで支払う必要がありますが、デバッグと保守が容易なコンパクトで読みやすいコードと引き換えに、この機能を無視する必要がある場合があります。 もちろん、この場合、標準的なリフレクションの手段を使用してコード生成を適用したり、場合によっては最初は動的な別の言語(同じPython)を使用したりすることもできますが、私見では、この場合のスピーカーの使用が最もシンプルで最も美しいソリューションです。

ここからコードをダウンロードできます(60 Kb)。

プログイット






All Articles