转换表达式树

本文介绍如何在生成该表达式树的修改副本时访问表达式树中的每个节点。 为了理解算法,您需要转换表达式树,以便将其翻译到另一个环境中。 你更改了已创建的算法。 这可能是为了添加日志记录、拦截方法调用并跟踪它们,或其他目的。

用于转换表达式树的代码是您已经见过的用于访问树中所有节点的功能的扩展。 转换表达式树时,将访问所有节点,并在访问它们时生成新树。 新树可能包含对原始节点的引用,或已放置在树中的新节点。

让我们访问一个表达式树,并使用一些替换节点创建新树。 在此示例中,我们将任何常量替换为大于 10 倍的常量。 否则,请将表达式树保留原样。 无需读取常量的值并将其替换为新常量,而是通过将常量节点替换为执行乘法的新节点来进行此替换。

在这里,找到常量节点后,将创建一个新的乘法节点,其子级为原始常量和常量 10

private static Expression ReplaceNodes(Expression original)
{
    if (original.NodeType == ExpressionType.Constant)
    {
        return Expression.Multiply(original, Expression.Constant(10));
    }
    else if (original.NodeType == ExpressionType.Add)
    {
        var binaryExpression = (BinaryExpression)original;
        return Expression.Add(
            ReplaceNodes(binaryExpression.Left),
            ReplaceNodes(binaryExpression.Right));
    }
    return original;
}

通过用替换节点替换原始节点来创建新树。 通过编译和执行替换的树来验证更改。

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));
var addition = Expression.Add(one, two);
var sum = ReplaceNodes(addition);
var executableFunc = Expression.Lambda(sum);

var func = (Func<int>)executableFunc.Compile();
var answer = func();
Console.WriteLine(answer);

生成新树是访问现有树中的节点以及创建新节点并将其插入树的组合。 上一个示例显示了表达式树不可变的重要性。 请注意,在前面的代码中创建的新树包含新创建的节点和现有树中的节点的混合体。 节点可用于这两个树,因为现有树中的节点无法修改。 重用节点可显著提高内存效率。 同一节点可以在整个树中或多个表达式树中使用。 由于无法修改节点,因此只要需要,就可以重复使用同一个节点。

遍历并执行加法

让我们通过生成遍历加法节点的树并计算结果的第二个访问者来对新树进行验证。 对目前见到的访问者进行一些修改。 在此新版本中,访问者将返回到目前为止加法运算的部分总和。 对于常量表达式,它只是常量表达式的值。 对于加法表达式,遍历这些树后,其结果为左操作数和右操作数的总和。

var one = Expression.Constant(1, typeof(int));
var two = Expression.Constant(2, typeof(int));
var three = Expression.Constant(3, typeof(int));
var four = Expression.Constant(4, typeof(int));
var addition = Expression.Add(one, two);
var add2 = Expression.Add(three, four);
var sum = Expression.Add(addition, add2);

// Declare the delegate, so you can call it
// from itself recursively:
Func<Expression, int> aggregate = null!;
// Aggregate, return constants, or the sum of the left and right operand.
// Major simplification: Assume every binary expression is an addition.
aggregate = (exp) =>
    exp.NodeType == ExpressionType.Constant ?
    (int)((ConstantExpression)exp).Value :
    aggregate(((BinaryExpression)exp).Left) + aggregate(((BinaryExpression)exp).Right);

var theSum = aggregate(sum);
Console.WriteLine(theSum);

这里有相当多的代码,但概念容易理解。 此代码访问首次深度搜索后的子级。 遇到常量节点时,访问者返回常量的值。 访问者访问这两个子级之后,这些子级计算出了为该子树计算的总和。 加法节点现在可以计算其总和。 访问表达式树中的所有节点后,将计算总和。 可以通过在调试器中运行示例来跟踪代码执行。

让我们更轻松地跟踪节点是如何被分析的,以及通过遍历树来计算总和的方法。 下面是 Aggregate 方法的更新版本,其中包含相当多的跟踪信息:

private static int Aggregate(Expression exp)
{
    if (exp.NodeType == ExpressionType.Constant)
    {
        var constantExp = (ConstantExpression)exp;
        Console.Error.WriteLine($"Found Constant: {constantExp.Value}");
        if (constantExp.Value is int value)
        {
            return value;
        }
        else
        {
            return 0;
        }
    }
    else if (exp.NodeType == ExpressionType.Add)
    {
        var addExp = (BinaryExpression)exp;
        Console.Error.WriteLine("Found Addition Expression");
        Console.Error.WriteLine("Computing Left node");
        var leftOperand = Aggregate(addExp.Left);
        Console.Error.WriteLine($"Left is: {leftOperand}");
        Console.Error.WriteLine("Computing Right node");
        var rightOperand = Aggregate(addExp.Right);
        Console.Error.WriteLine($"Right is: {rightOperand}");
        var sum = leftOperand + rightOperand;
        Console.Error.WriteLine($"Computed sum: {sum}");
        return sum;
    }
    else throw new NotSupportedException("Haven't written this yet");
}

sum 表达式上运行它将生成以下输出:

10
Found Addition Expression
Computing Left node
Found Addition Expression
Computing Left node
Found Constant: 1
Left is: 1
Computing Right node
Found Constant: 2
Right is: 2
Computed sum: 3
Left is: 3
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 3
Left is: 3
Computing Right node
Found Constant: 4
Right is: 4
Computed sum: 7
Right is: 7
Computed sum: 10
10

跟踪输出,并在上面的代码中跟随。 你应该能够找出代码如何在遍历树的过程中访问每个节点并计算总和。

现在,让我们看一下另一个执行,其表达式由 sum1 给出。

Expression<Func<int>> sum1 = () => 1 + (2 + (3 + 4));

下面是通过检查此表达式得到的输出:

Found Addition Expression
Computing Left node
Found Constant: 1
Left is: 1
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 2
Left is: 2
Computing Right node
Found Addition Expression
Computing Left node
Found Constant: 3
Left is: 3
Computing Right node
Found Constant: 4
Right is: 4
Computed sum: 7
Right is: 7
Computed sum: 9
Right is: 9
Computed sum: 10
10

虽然最终答案相同,但树遍历不同。 节点按不同的顺序遍历,因为树是由于先进行了不同的操作而构建的。

创建修改的副本

创建新的 控制台应用程序 项目。 将using指令添加到用于System.Linq.Expressions命名空间的文件中。 将 AndAlsoModifier 类添加到项目。

public class AndAlsoModifier : ExpressionVisitor
{
    public Expression Modify(Expression expression)
    {
        return Visit(expression);
    }

    protected override Expression VisitBinary(BinaryExpression b)
    {
        if (b.NodeType == ExpressionType.AndAlso)
        {
            Expression left = this.Visit(b.Left);
            Expression right = this.Visit(b.Right);

            // Make this binary expression an OrElse operation instead of an AndAlso operation.
            return Expression.MakeBinary(ExpressionType.OrElse, left, right, b.IsLiftedToNull, b.Method);
        }

        return base.VisitBinary(b);
    }
}

此类继承 ExpressionVisitor 类,并且专用于修改表示条件 AND 运算的表达式。 它将这些操作从条件 AND 更改为条件 OR。 该类重写了基类型的 VisitBinary 方法,因为条件 AND 表达式被表示为二进制表达式。 在VisitBinary方法中,如果传递给它的表达式表示条件AND运算,则代码会构造一个新的表达式,其中包含条件OR运算符,而不是条件AND运算符。 如果传递给 VisitBinary 的表达式不表示条件 AND 操作,该方法将使用基类的实现。 基类方法构造类似于传入的表达式树的节点,但节点的子树替换为访问者以递归方式生成的表达式树。

using指令添加到用于System.Linq.Expressions命名空间的文件中。 将代码添加到 Main Program.cs 文件中的方法,以创建表达式树并将其传递给修改它的方法。

Expression<Func<string, bool>> expr = name => name.Length > 10 && name.StartsWith("G");
Console.WriteLine(expr);

AndAlsoModifier treeModifier = new AndAlsoModifier();
Expression modifiedExpr = treeModifier.Modify((Expression)expr);

Console.WriteLine(modifiedExpr);

/*  This code produces the following output:

    name => ((name.Length > 10) && name.StartsWith("G"))
    name => ((name.Length > 10) || name.StartsWith("G"))
*/

该代码创建一个包含条件 AND 操作的表达式。 然后,它会创建类的 AndAlsoModifier 实例,并将表达式传递给 Modify 此类的方法。 输出原始表达式树和修改后的表达式树以显示更改。 编译并运行应用程序。

了解详细信息

此示例显示了要生成的代码的小子集,用于遍历和解释表达式树所表示的算法。 有关构建将表达式树转换为另一种语言的通用库的信息,请阅读马特·沃伦 的这一系列 。 其中详细介绍了如何转换表达式树中可能找到的任何代码。

你现在已经见识到了表达式树的真正强大功能。 检查一组代码,对该代码进行所需的任何更改,并执行修改后的版本。 由于表达式树是不可变的,因此使用现有树的组件创建新树。 重用节点可最大程度地减少创建修改后的表达式树所需的内存量。