在表达式树中组合2个以上的表达式

时间:2015-07-31 06:41:38

标签: c# expression-trees

我正在尝试使用表达式树来创建动态linq查询。

我想知道如何使用AND / OR组合3个表达式。

假设我有3个表达式a,b,c,如果我使用

Expression z = Expression.Or(a,b); 
Expression result = Expression.And(z,c);

我得到的结果是(a或b)和c。但是,我想要(a或b和c)。这甚至可能吗?

修改 我会再试一次。 我有表达式来创建我的动态查询。我的源数据是XML,如下所示

  <Filter>      
    <Expression>
      <operator>
        Equal
      </operator>
      <Left>
        <property>RecStatus</property>            
      </Left>

      <Right>
        <constant type="char">D</constant>            
      </Right>
    </Expression>
</Filter>

每个过滤器可以有很多表达式,我解析这个xml并创建动态表达式。但是,当我组合多于2个表达式时,它们会出现在括号中,例如。 (A和B)当我尝试追加第三个时,结果变为((A和B)和C)并且这继续。我希望能够控制括号的应用方式,因为这会改变运算符的优先级(除非创建XML的用户改变了最终效果)。我想知道是否有办法控制这种行为或是否有另一种(更好)的方法来做到这一点。

3 个答案:

答案 0 :(得分:0)

使用表达式模型就像使用任何语言创建表达式一样。当然,如果表达式太复杂,你可以使用临时变量:

 Expression result = Expression.Or(a, Expression.And(b, c));

在数学方面,这实际上是波兰表示法,其中操作形式(对于二元运算符)是<operator> <operand1> <operand2>,如+ 3 4OR a b。或OR a AND b c。关于波兰表示法,请开始阅读here

答案 1 :(得分:0)

表达式始终由operator precedence评估。相同的运算符通常是从左到右的关联和评估。

因此,对于a || b && c的示例,由于条件And的优先级高于条件Or,因此表达式等效于a || (b && c)。这是由C#语法的语法保证的。所以正确的表达方式是Expression.Or(a, Expression.And(b, c))

同样,所有这些表达式都可以简化为一系列2-ary运算。请注意,这实际上可能是一个好处,因为您可以尽可能明确,并且您不需要记住所有内容的确切运算符优先级。

答案 2 :(得分:0)

poke's answer中,System.Linq.Expressions.Expression树是由Expression.BinaryExpression和/或单分支Expression类型的连接实例组成的集合,这些树称为“ < em> non-terminals ”,以及其他自赋值的Expression.Expression实例( terminals 或“叶子”节点),并由专有根引用。参照其一般性,大多数表达式树可以非正式地称为 左分支 右分支 根下方的整体分支方向。

由于对表达式树根节点的评估取决于其子节点的值,因此对根节点的评估是 last 。这意味着 C#语句以及其他受 左右运算符优先级 限制的串联运算符的线性/书面序列通常表示为左分支表达式树。

例如,由于表达式48 - 1 - 2 - 3需要被评估为(((48 - 1) - 2) - 3),因此它必须具有此左分支结构才能获得42的正确答案:

enter image description here

对于同一表达式48 - 1 - 2 - 3右分支树暗含(48 - (1 - (2 - 3))),它给出了不同的错误答案-48

enter image description here

事实证明,这意味着给定一个具有从左到右优先级的典型表达式-无论是书面的,机器可读的String还是线性,有序运算符序列的其他表示形式, -您可以按照给定的顺序从左到右进行扫描,从而构建具有正确运算符优先级的树,然后对于每次迭代,将前一个节点向下推到BinaryExpression.Left端。

由于错误的运算符优先级只会对commutative operators产生错误的结果,因此我们将以减法为例。如上所述,对于正常的前向扫描情况,一个简单的循环将构建正确的左分支表达式树。

static Expression L_to_R_Forward(IEnumerable<Expression> seq)
{
    Expression root = null;
    foreach (var cur in seq)
        root = root == null ? cur : Expression.Subtract(root, cur);

    return root;
}

为演示此方法的使用,我们从所需的左右表达式48 - 1 - 2 - 3的自然表示开始,该表达式是有序序列{ 48, 1, 2, 3 }。我们将其转换为整数ConstantExpression实例的序列,然后在运行时将其传递给上面显示的方法以获取所需的Expression根。然后,将其编译成委托func,您可以调用该委托(如果愿意,可以重复多次)以获取结果。

var seq = new[] { 48, 1, 2, 3 }.Select(i => Expression.Constant(i));

var root = L_to_R_Forward(seq);  // root <- '((48 - 1) - 2) - 3'

var func = Expression.Lambda<Func<int>>(root).Compile();

int result = func();          // result <-- 42 (ok!)

这一切都很好,因此在这种最常见的情况下,一个简单的循环就足够了,您不必使用递归(一种常与树构建相关的技术)来获取正确的树。

  

旁注-和一种方便的实用程序方法?


这是为该方法调用的方法生成的已编译IL代码刚刚显示了func名代表。

IL_0000: ldc.i4.s   48
IL_0002: ldc.i4.1   
IL_0003: sub        
IL_0004: ldc.i4.2   
IL_0005: sub        
IL_0006: ldc.i4.3   
IL_0007: sub        
IL_0008: ret  
     

请注意,尽管我们的树仅包含ConstantExpression个值,这些值是整数常量,但是Expression.Subtract和大多数其他BinaryExpression类型(如果不是全部)不能识别出LeftRight都是可简化的数值常量。

     

这可以通过扩展方法实用程序来补救,该实用程序将在可能的情况下执行此减少操作:

public static Expression ReduceConstants(this Expression expr)
{
    if (expr is ConstantExpression)
        return expr;

    if ((expr = expr.Reduce()) is BinaryExpression be &&
        (be.Left.ReduceConstants()) is ConstantExpression &&
        (be.Right.ReduceConstants()) is ConstantExpression)
    {
        var Tdel = typeof(Func<>).MakeGenericType(expr.Type);
        var del = Expression.Lambda(Tdel, be).Compile();
        return Expression.Constant(del.DynamicInvoke(), expr.Type);
    }
    return expr;
}
     

在原始示例中,在提取lambda之前仅调用一次此扩展方法,从而进一步简化了表达式,因此现在可以提供功能上相当的简化代码。

var seq = new[] { 48, 1, 2, 3 }.Select(i => Expression.Constant(i));
var root = L_to_R_Forward(seq);  // '((48 - 1) - 2) - 3' 
root = root.ReduceConstants();   // '(42)'               <--- added step
var func = Expression.Lambda<Func<int>>(root).Compile();
int result = func();             // result <-- 42 (ok!)
     

这里是IL:

IL_0000: ldc.i4.s   42
IL_0002: ret        

现在让我们考虑一下您当然仍然需要左分支表达式树的正确行为的情况,但是恰好出现了 线性操作序列 的顺序相反,这意味着它仍然受左到右运算符优先级的约束。在这种情况下,上述步骤将不起作用,您需要以某种方式进行相应的调整。回到该示例,如果我们反转原始序列,但仍然希望按原始顺序排列树,则会出现错误:

seq = seq.Reverse();  // now: { 3, 2, 1, 48 }

root = Typed.L_to_R_Forward(seq);       // '(((3 - 2) - 1) - 48)'

func = Expression.Lambda<Func<int>>(root).Compile();

result = func(); // result <-- -48 (ERROR)

典型的计算机科学入门课程的学生知道,解决这种情况并从相反的顺序中获取正确的树的一种方法是通过递归,您可以编写一个始终假定它正在构建函数的函数。根节点。如果此类函数发现由于缺少某些子项而无法完成,则只需调用自身即可获取它们。

在当前示例中,递归解决方案会将Expression的{​​{1}}暂时保存在递归堆栈帧中,直到其同级{{1 }} 3可用。在递归树构建中,构造父节点的时间推迟到确定其所有子节点最终确定为止。从某种意义上讲,建立根节点是第一步,最后一步和唯一步骤。

尽管反向树构建的递归解决方案很简单,而且实际上 经典优雅 ,但也有可能使用一个简单的循环来获得正确的结果沿相反方向对树进行变异。

不幸的是,Expression的{​​{1}}和((48 - 1) - 2)属性是只读的,因此通常您不能对表达式树进行突变。最终树或编译后的树可能会具有不变性,但是在这种情况下,您也不允许在构建树的过程中对它进行突变。但是,表达式确实支持“归约”的概念,该概念提供了一种重写方法。

以下简单类是“扩展表达式”,它实现了Left树的简单可变节点。您可以将此自定义Right的实例设置到任何表达式树中,然后将其BinaryExpression属性设置为实际所需的表达式。它充当真正所需节点的 代理 。但是,与ExpressionExpression不同的是,它的Node属性是可读写的,因此您可以在调用ConstantExpression之前的任何时间更改代理表达式的值。在实际评估中。

BinaryExpression

我将Node符号放在代理节点的打印输出上,以便您可以看到它们在表达式树打印输出中的嵌入位置。上面的类可完美地用于这些演示目的,因此也大大简化了。您可以想象这里没有显示很多改进。

现在,我们可以使用直接的命令式代码(而不是通过重写器类或使用更通用的Reduce实例)显式地重新排列表达式树的节点,从而直接对表达式树进行变异。现在,我们可以为树构建器示例编写正确的非递归版本。

(请注意,始终假设使用 C#7 ;尤其是通过使用'ref locals',可以大大简化此下一个功能。)

public class ExpressionRef : Expression
{
    public override ExpressionType NodeType => ExpressionType.Extension;
    public override Type Type => Node.Type;
    public override bool CanReduce => true;

    public Expression Node;
    public override Expression Reduce() => Node;

    public override String ToString() => "&" + Node;
};

最后,显示此功能的代码以相反的顺序读取节点时,会为我们提供正确的树。

'&'