【问题标题】:Declare variable in LinQ expression tree在 LinQ 表达式树中声明变量
【发布时间】:2014-02-03 11:12:46
【问题描述】:

在上一个问题(Refine Enumerable LINQ WHERE statement with repeated parameters)中,我通过使用let 语句声明变量来解决问题,我的代码如下所示:

return
from q in pList
let currentContract = q.StaffContracts.OrderByDescending(p => p.SignedDate).Where(p => p.Active).FirstOrDefault()
where (currentContract.Timespan >= fromValue && currentContract.Timespan <= toValue)
select q;

在这种情况下,currentContract 是重复的表达式。现在我有一个新问题。我的方法既不提供也不请求 IQueryable,但它需要 Expression&lt;Func&lt;Staff, bool&gt;&gt; 作为返回(意思是,只有 WHERE 子句)。

我试过了,但没有成功:

return q =>
{
    var currentContract = q.StaffContracts.OrderByDescending(p => p.SignedDate).Where(p => p.Active).FirstOrDefault();
    return currentContract != null && currentContract.Timespan >= fromValue && currentContract.Timespan <= toValue;
};

编译错误信息是:

带有语句体的 lambda 表达式不能转换为表达式树

有什么解决办法吗?

【问题讨论】:

标签: c# .net linq lambda expression


【解决方案1】:

所以我们可以在这里做的是创建一个Compose 方法,它接受一个Expression 代表一个带有一个参数和一个返回值的 lambda,然后是第二个 lambda,它将第一个 lambda 的输出作为输入,然后返回一个新的 lambda,它接受第一个参数的输入并返回第二个参数的输出。

如果它只是普通的委托,则该方法看起来像这样(只是为了让您了解它在概念上所做的事情):

public static Func<TFirst, TResult> Compose<TFirst, TIntermediate, TResult>(
    Func<TFirst, TIntermediate> first,
    Func<TIntermediate, TResult> second)
{
    return firstParam => second(first(firstParam));
}

这在Expression 对象中实现,有点复杂:

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

这使用以下方法将一个Expression 的所有实例替换为另一个:

internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

现在我们有了Compose 方法,我们可以编写一个表达式,它接受你的一个项目并返回一个Contract,然后编写另一个方法来接受该合同并计算一个bool 指示它是否有效:

public static Expression<Func<Item, bool>> GetFilter(
    TimeSpan fromValue, TimeSpan toValue)
{
    Expression<Func<Item, Contract>> currentContract =
        q => q.StaffContracts
                .OrderByDescending(p => p.SignedDate)
                .Where(p => p.Active)
                .FirstOrDefault();

    return currentContract.Compose(contract =>
        contract != null &&
        contract.TimeSpan >= fromValue &&
        contract.TimeSpan <= toValue);
}

这里的 Compose 方法将在内部将第二个 lambda 中的所有 contract 实例替换为 currentContract 的主体。所以效果是如果你把它写了三遍,即使这样你就不需要这样做了。

这个Compose 方法是您可以随时在表达式树中创建变量时使用的东西(查询提供程序不支持的东西)。您始终可以创建一个计算变量值的方法,然后 Compose 在另一个表达式中使用它。

【讨论】:

  • 非常感谢!花了我差不多半个小时才明白,但它确实有效,而且我知道扩展方法的美妙使用。
【解决方案2】:

恐怕您必须使用System.Linq.Expressions.Expression 类方法手动准备表达式:

public static Expression<Func<Item, bool>> GetWhere(TimeSpan fromValue, TimeSpan toValue)
{
    Expression<Func<Item, Contract>> currentContract =
        q => q.StaffContracts
              .OrderByDescending(p => p.SignedDate)
              .Where(p => p.Active)
              .FirstOrDefault();

    var param = currentContract.Parameters.First();

    return Expression.Lambda<Func<Item, bool>>(
                Expression.And(
                    Expression.And(
                        Expression.NotEqual(
                            currentContract,
                            Expression.Constant(null, typeof(Contract))),
                        Expression.GreaterThanOrEqual(
                            Expression.Property(currentContract, "Timespan"),
                            Expression.Constant(fromValue))),
                    Expression.LessThanOrEqual(
                        Expression.Property(currentContract, "Timespan"),
                        Expression.Constant(toValue))),
                param);
}

【讨论】:

  • 您正在对currentContract 进行操作,就好像它是Contract 对象一样,但事实并非如此。这是一个表示函数的表达式,因此您的代码将不起作用。也许你是想抓住那个表情的身体?
  • 我没有对此进行测试,但我希望它与return q =&gt; q.StaffContracts.OrderByDescending(p =&gt; p.SignedDate).Where(p =&gt; p.Active).FirstOrDefault() != null &amp;&amp; q.StaffContracts.OrderByDescending(p =&gt; p.SignedDate).Where(p =&gt; p.Active).FirstOrDefault().Timespan &gt; fromValue &amp;&amp; q.StaffContracts.OrderByDescending(p =&gt; p.SignedDate).Where(p =&gt; p.Active).FirstOrDefault().Timespan &lt; toValue; 相同,这就是我希望它能够工作的原因。
  • 但事实并非如此。目前,您正在将 a function 与 null 和恒定时间跨度进行比较。当表达式在运行时创建时,这将失败,因为somefunction &gt;= sometimespan 的整个想法毫无意义。根据您的评论,您认为它会产生什么并不是它会做什么。它只会抛出一个错误。正如我所说,您似乎打算在所有这些地方使用 lambda 的主体,而不是 lambda 本身。
猜你喜欢
  • 2013-05-24
  • 1970-01-01
  • 2011-12-11
  • 1970-01-01
  • 1970-01-01
  • 2011-12-12
相关资源
最近更新 更多