41

次の式を表す式ツリーを作成しようとしています。

myObject.childObjectCollection.Any(i => i.Name == "name");

わかりやすくするために短縮しました。次のとおりです。

//'myObject.childObjectCollection' is represented here by 'propertyExp'
//'i => i.Name == "name"' is represented here by 'predicateExp'
//but I am struggling with the Any() method reference - if I make the parent method
//non-generic Expression.Call() fails but, as per below, if i use <T> the 
//MethodInfo object is always null - I can't get a reference to it

private static MethodCallExpression GetAnyExpression<T>(MemberExpression propertyExp, Expression predicateExp)
{
    MethodInfo method = typeof(Enumerable).GetMethod("Any", new[]{ typeof(Func<IEnumerable<T>, Boolean>)});
    return Expression.Call(propertyExp, method, predicateExp);
}

私は何が間違っているのですか?誰か提案がありますか?

4

2 に答える 2

85

あなたのやり方にはいくつかの間違いがあります。

  1. 抽象化レベルを混合しています。への T パラメータはGetAnyExpression<T>、 のインスタンス化に使用される型パラメータとは異なる場合がありますpropertyExp.Type。T 型パラメーターは、抽象化スタックでコンパイル時に一歩近づきます。GetAnyExpression<T>リフレクション経由で呼び出していない限り、コンパイル時に決定されますが、渡された式に埋め込まれた型propertyExpは実行時に決定されます。述語を an として渡すことExpressionも、抽象化の混同です。これが次のポイントです。

  2. を呼び出そうとしているので、渡す述語は、いかなる種類のものでもGetAnyExpressionなく、デリゲート値である必要があります。の式ツリー バージョンを呼び出そうとした場合は、代わりにa を渡す必要があります。これは引用符で囲んでおり、式よりも具体的な型を渡すことが正当化されるまれなケースの 1 つです。次のポイントに進みます。ExpressionEnumerable.Any<T>AnyLambdaExpression

  3. 一般に、値を渡す必要がありExpressionます。一般的に式ツリーを操作する場合 (これは LINQ とその仲間だけでなく、あらゆる種類のコンパイラに適用されます)、操作しているノード ツリーの直接的な構成にとらわれない方法で行う必要があります。を呼び出していると仮定していますが、実際には を扱っていることを知る必要はありません。これは、コンパイラ AST の基本に慣れていない人によくある間違いです。フランス・ボウマAnyMemberExpressionMemberExpressionExpressionIEnumerable<>彼が最初に式ツリーを扱い始めたとき、同じ過ちを繰り返しました - 特別な場合を考えて。一般的に考えてください。中期的および長期的には、多くの手間を省くことができます。

  4. そして、ここにあなたの問題の要点があります (ただし、2 番目とおそらく最初の問題は、あなたがそれを乗り越えていた場合にあなたを悩ませていたでしょう) - Any メソッドの適切なジェネリック オーバーロードを見つけて、正しい型でインスタンス化する必要があります。リフレクションは、ここで簡単に解決することはできません。反復して適切なバージョンを見つける必要があります。

したがって、それを分解すると、ジェネリック メソッド ( Any) を見つける必要があります。これを行うユーティリティ関数を次に示します。

static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs, 
    Type[] argTypes, BindingFlags flags)
{
    int typeArity = typeArgs.Length;
    var methods = type.GetMethods()
        .Where(m => m.Name == name)
        .Where(m => m.GetGenericArguments().Length == typeArity)
        .Select(m => m.MakeGenericMethod(typeArgs));

    return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
}

ただし、型引数と正しい引数の型が必要です。あなたからそれを取得することpropertyExp Expressionは完全に簡単ではありません。なぜなら、は型または他の型でExpressionある可能性があるためです。しかし、インスタンス化を見つけてその型引数を取得する必要があります。私はそれをいくつかの関数にカプセル化しました:List<T>IEnumerable<T>

static bool IsIEnumerable(Type type)
{
    return type.IsGenericType
        && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}

static Type GetIEnumerableImpl(Type type)
{
    // Get IEnumerable implementation. Either type is IEnumerable<T> for some T, 
    // or it implements IEnumerable<T> for some T. We need to find the interface.
    if (IsIEnumerable(type))
        return type;
    Type[] t = type.FindInterfaces((m, o) => IsIEnumerable(m), null);
    Debug.Assert(t.Length == 1);
    return t[0];
}

したがって、 any が与えられた場合、インスタンス化をそこからType引き出すことができIEnumerable<T>ます。(正確に) 存在しない場合はアサートします。

その作業が邪魔にならないので、実際の問題を解決することはそれほど難しくありません。メソッドの名前を CallAny に変更し、パラメーターの型を提案どおりに変更しました。

static Expression CallAny(Expression collection, Delegate predicate)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType);

    Type elemType = cType.GetGenericArguments()[0];
    Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

    // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
    MethodInfo anyMethod = (MethodInfo)
        GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
            new[] { cType, predType }, BindingFlags.Static);

    return Expression.Call(
        anyMethod,
            collection,
            Expression.Constant(predicate));
}

Main()上記のすべてのコードを使用して、簡単なケースで機能することを確認するルーチンを次に示します。

static void Main()
{
    // sample
    List<string> strings = new List<string> { "foo", "bar", "baz" };

    // Trivial predicate: x => x.StartsWith("b")
    ParameterExpression p = Expression.Parameter(typeof(string), "item");
    Delegate predicate = Expression.Lambda(
        Expression.Call(
            p,
            typeof(string).GetMethod("StartsWith", new[] { typeof(string) }),
            Expression.Constant("b")),
        p).Compile();

    Expression anyCall = CallAny(
        Expression.Constant(strings),
        predicate);

    // now test it.
    Func<bool> a = (Func<bool>) Expression.Lambda(anyCall).Compile();
    Console.WriteLine("Found? {0}", a());
    Console.ReadLine();
}
于 2008-11-28T19:32:07.317 に答える
19

バリーの答えは、元のポスターによって提起された質問に対する有効な解決策を提供します。質問と回答をしてくれたこれらの個人の両方に感謝します。

Any() メソッドの呼び出しを含む式ツリーをプログラムで作成するという、非常によく似た問題の解決策を考案しようとしていたときに、このスレッドを見つけました。ただし、追加の制約として、私のソリューションの最終的な目標は、Any() 評価の作業が実際に DB 自体で実行されるように、動的に作成された式を Linq-to-SQL を介して渡すことでした。

残念ながら、これまでに説明したソリューションは、Linq-to-SQL で処理できるものではありません。

これが動的な式ツリーを構築したいという非常に一般的な理由である可能性があるという仮定の下で動作し、私は自分の調査結果をスレッドに追加することにしました。

Barry の CallAny() の結果を Linq-to-SQL Where() 句の式として使用しようとすると、次のプロパティを持つ InvalidOperationException を受け取りました。

  • HResult=-2146233079
  • Message="内部 .NET Framework データ プロバイダー エラー 1025"
  • ソース=System.Data.Entity

ハードコードされた式ツリーと、CallAny() を使用して動的に作成された式ツリーを比較した結果、中核的な問題は、述語式の Compile() と、CallAny() で結果のデリゲートを呼び出そうとしたことが原因であることがわかりました。Linq-to-SQL の実装の詳細を掘り下げることなく、Linq-to-SQL がそのような構造をどう処理すればよいか分からないのは理にかなっているように思えました。

したがって、いくつかの実験の後、提案された CallAny() 実装を少し修正して、Any() 述語ロジックのデリゲートではなく predicateExpression を取ることで、目的の目標を達成することができました。

私の修正された方法は次のとおりです。

static Expression CallAny(Expression collection, Expression predicateExpression)
{
    Type cType = GetIEnumerableImpl(collection.Type);
    collection = Expression.Convert(collection, cType); // (see "NOTE" below)

    Type elemType = cType.GetGenericArguments()[0];
    Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));

    // Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
    MethodInfo anyMethod = (MethodInfo)
        GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType }, 
            new[] { cType, predType }, BindingFlags.Static);

    return Expression.Call(
        anyMethod,
        collection,
        predicateExpression);
}

次に、EF での使用法を示します。わかりやすくするために、まず、使用しているおもちゃのドメイン モデルと EF コンテキストを示します。基本的に、私のモデルは単純なブログと投稿のドメインです...ブログには複数の投稿があり、各投稿には日付があります:

public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }

    public virtual List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public DateTime Date { get; set; }

    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; }
}

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

そのドメインが確立されたら、最終的に改訂された CallAny() を実行し、Linq-to-SQL に Any() を評価する作業を実行させるコードを次に示します。私の特定の例では、指定された締め切り日よりも新しい投稿が少なくとも 1 つあるすべてのブログを返すことに焦点を当てています。

static void Main()
{
    Database.SetInitializer<BloggingContext>(
        new DropCreateDatabaseAlways<BloggingContext>());

    using (var ctx = new BloggingContext())
    {
        // insert some data
        var blog  = new Blog(){Name = "blog"};
        blog.Posts = new List<Post>() 
            { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
        blog.Posts = new List<Post>()
            { new Post() { Title = "p2", Date = DateTime.Parse("01/01/2002") } };
        blog.Posts = new List<Post>() 
            { new Post() { Title = "p3", Date = DateTime.Parse("01/01/2003") } };
        ctx.Blogs.Add(blog);

        blog = new Blog() { Name = "blog 2" };
        blog.Posts = new List<Post>()
            { new Post() { Title = "p1", Date = DateTime.Parse("01/01/2001") } };
        ctx.Blogs.Add(blog);
        ctx.SaveChanges();


        // first, do a hard-coded Where() with Any(), to demonstrate that
        // Linq-to-SQL can handle it
        var cutoffDateTime = DateTime.Parse("12/31/2001");
        var hardCodedResult = 
            ctx.Blogs.Where((b) => b.Posts.Any((p) => p.Date > cutoffDateTime));
        var hardCodedResultCount = hardCodedResult.ToList().Count;
        Debug.Assert(hardCodedResultCount > 0);


        // now do a logically equivalent Where() with Any(), but programmatically
        // build the expression tree
        var blogsWithRecentPostsExpression = 
            BuildExpressionForBlogsWithRecentPosts(cutoffDateTime);
        var dynamicExpressionResult = 
            ctx.Blogs.Where(blogsWithRecentPostsExpression);
        var dynamicExpressionResultCount = dynamicExpressionResult.ToList().Count;
        Debug.Assert(dynamicExpressionResultCount > 0);
        Debug.Assert(dynamicExpressionResultCount == hardCodedResultCount);
    }
}

BuildExpressionForBlogsWithRecentPosts() は、次のように CallAny() を使用するヘルパー関数です。

private Expression<Func<Blog, Boolean>> BuildExpressionForBlogsWithRecentPosts(
    DateTime cutoffDateTime)
{
    var blogParam = Expression.Parameter(typeof(Blog), "b");
    var postParam = Expression.Parameter(typeof(Post), "p");

    // (p) => p.Date > cutoffDateTime
    var left = Expression.Property(postParam, "Date");
    var right = Expression.Constant(cutoffDateTime);
    var dateGreaterThanCutoffExpression = Expression.GreaterThan(left, right);
    var lambdaForTheAnyCallPredicate = 
        Expression.Lambda<Func<Post, Boolean>>(dateGreaterThanCutoffExpression, 
            postParam);

    // (b) => b.Posts.Any((p) => p.Date > cutoffDateTime))
    var collectionProperty = Expression.Property(blogParam, "Posts");
    var resultExpression = CallAny(collectionProperty, lambdaForTheAnyCallPredicate);
    return Expression.Lambda<Func<Blog, Boolean>>(resultExpression, blogParam);
}

注: ハードコーディングされた式と動的に構築された式の間に、一見重要ではないように見えるデルタがもう 1 つ見つかりました。動的に構築されたものには、ハードコードされたバージョンにはないように見える(または必要としない)「追加の」変換呼び出しがあります。変換は CallAny() 実装で導入されています。Linq-to-SQL はそれで問題ないように思われるので、そのまま残しました (不要ではありましたが)。おもちゃのサンプルよりも堅牢な用途でこの変換が必要になるかどうかは、完全にはわかりませんでした。

于 2013-08-08T14:54:05.273 に答える