为什么要引用LambdaExpression?

时间:2015-05-07 15:14:32

标签: c# linq lambda linq-expressions custom-linq-providers

我已经阅读了this answer,并从中了解了它突出显示的具体情况,即当你在另一个lambda中有一个lambda并且你不想意外地让内部lambda也编译时与外面的。编译外部时,您希望内部lambda表达式保留为表达式树。在那里,是的,引用内在的lambda表达是有道理的。

但我相信这是关于它的。引用lambda表达式还有其他用例吗?

如果没有,为什么所有LINQ运算符,即IQueryable<T>类中声明的Queryable上的扩展引用它们作为参数接收的谓词或lambdas将该信息打包在MethodCallExpression

我尝试了一个例子(以及过去几天的其他几个例子),在这种情况下引用lambda似乎没有任何意义。

这是一个方法调用表达式,该方法需要一个lambda表达式(而不是委托实例)作为唯一参数。

然后我将MethodCallExpression包装在lambda中进行编译。

但是,这并没有编译内部LambdaExpressionGimmeExpression方法的参数)。它将内部lambda表达式保留为表达式树,并且不创建它的委托实例。

事实上,它在没有引用的情况下效果很好。

如果我引用了这个参数,它会中断并给出一个错误,表明我向GimmeExpression方法传递了错误的参数类型。

这笔交易是什么?这个引用的全部内容是什么?

private static void TestMethodCallCompilation()
{
    var methodInfo = typeof(Program).GetMethod("GimmeExpression", 
        BindingFlags.NonPublic | BindingFlags.Static);

    var lambdaExpression = Expression.Lambda<Func<bool>>(Expression.Constant(true));

    var methodCallExpression = Expression.Call(null, methodInfo, lambdaExpression);

    var wrapperLambda = Expression.Lambda(methodCallExpression);
    wrapperLambda.Compile().DynamicInvoke();
}

private static void GimmeExpression(Expression<Func<bool>> exp)
{
    Console.WriteLine(exp.GetType());
    Console.WriteLine("Compiling and executing expression...");
    Console.WriteLine(exp.Compile().Invoke());
}

1 个答案:

答案 0 :(得分:5)

您必须将参数作为ConstantExpression传递:

private static void TestMethodCallCompilation()
{
    var methodInfo = typeof(Program).GetMethod("GimmeExpression", 
        BindingFlags.NonPublic | BindingFlags.Static);

    var lambdaExpression = Expression.Lambda<Func<bool>>(Expression.Constant(true));

    var methodCallExpression = 
      Expression.Call(null, methodInfo, Expression.Constant(lambdaExpression));

    var wrapperLambda = Expression.Lambda(methodCallExpression);
    wrapperLambda.Compile().DynamicInvoke();
}

private static void GimmeExpression(Expression<Func<bool>> exp)
{
    Console.WriteLine(exp.GetType());
    Console.WriteLine("Compiling and executing expression...");
    Console.WriteLine(exp.Compile().Invoke());
}

原因应该非常明显 - 你传递一个常数值,所以它必须是ConstantExpression。通过直接传递表达式,您明确地说“并从这个复杂的表达式树中获取exp的值”。由于该表达式树实际上没有返回Expression<Func<bool>>的值,因此会出现错误。

IQueryable的工作方式与此无关。 IQueryable上的扩展方法必须保留有关表达式的所有信息 - 包括ParameterExpression和类似的类型和引用。这是因为他们实际上并没有任何东西 - 他们只是构建表达式树。当你致电queryable.Provider.Execute(expression)时,真正的工作就会发生。基本上,即使我们正在进行组合,而不是继承(/ interface implementation),这就是多态性的保留方式。但它确实意味着IQueryable扩展方法本身不能做任何快捷方式 - 他们对IQueryProvider实际解释查询的方式一无所知,所以他们不能扔掉任何东西

但是,从中获得的最重要的好处是,您可以编写查询和子查询。考虑这样的查询:

from item in dataSource
where item.SomeRelatedItem.Where(subItem => subItem.SomeValue == 42).Count() > 2
select item;

现在,这被翻译成这样的东西:

dataSource.Where(item => item.SomeRelatedItem.Where(subItem => subItem.SomeValue == 42).Count() > 2);

外部查询非常明显 - 我们将获得具有给定谓词的Where。但是,内部查询实际上将是CallWhere,将实际谓词作为参数。

通过确保Where方法的实际调用实际上被转换为Call方法的Where,这两种情况都变得相同,而您的LINQProvider就是那个有点简单:)

我实际编写的LINQ提供程序没有实现IQueryable,并且实际上在Where等方法中有一些有用的逻辑。它更简单,更有效,但有上述缺点 - 处理子查询的唯一方法是手动Invoke Call表达式来获得“真正的”谓词表达式。对于一个简单的LINQ查询来说,这是一个相当大的开销!

当然,它可以帮助您组合不同的可查询提供程序,尽管我实际上没有看到(m)在单个查询中使用两个完全不同的提供程序的任何示例。

至于Expression.ConstantExpression.Quote之间的区别,它们看起来非常相似。关键的区别在于Expression.Constant会将任何闭包视为实际的常量,而不是闭包。另一方面,Expression.Quote将保留闭包的“封闭性”。为什么?因为闭包对象本身传递为Expression.Constant :)并且因为IQueryable树正在做lambdas of lambdas of lambdas [...],你真的不希望在任何时候丢失闭包语义。