插入表达式树——如何获得每个子树的计算结果

本文关键字:计算 结果 何获得 表达式 插入 | 更新日期: 2023-09-27 18:25:27

我正在表达式树中做一些工作,这是一个规则引擎。

当你在表达式树上调用ToString()时,你会得到一个可爱的诊断文本:

 ((Param_0.Customer.LastName == "Doe") 
     AndAlso ((Param_0.Customer.FirstName == "John") 
     Or (Param_0.Customer.FirstName == "Jane")))

我写了这段代码,试图用一些日志功能包装表达式:

public Expression WithLog(Expression exp)
{
    return Expression.Block(Expression.Call(
        typeof (Debug).GetMethod("Print",
            new Type [] { typeof(string) }),
            new [] { Expression.Call(Expression.Constant(exp),
            exp.GetType().GetMethod("ToString")) } ), exp);
}

这应该允许我在表达式树的各个位置插入日志记录,并在执行表达式树时获得中间ToString()结果。

我还没有完全弄清楚如何获得每个子表达式的计算结果,并将其包含在日志输出中。理想情况下,出于诊断和审计目的,我希望看到类似这样的输出:

Executing Rule: (Param_0.Customer.LastName == "Doe") --> true
Executing Rule: (Param_0.Customer.FirstName == "John") --> true
Executing Rule: (Param_0.Customer.FirstName == "Jane") --> false
Executing Rule: (Param_0.Customer.FirstName == "John") Or (Param_0.Customer.FirstName == "Jane")) --> true
Executing Rule: (Param_0.Customer.LastName == "Doe") AndAlso ((Param_0.Customer.FirstName == "John") Or (Param_0.Customer.FirstName == "Jane")) --> true

我怀疑我要么需要使用ExpressionVisitor遍历树并向每个节点添加一些代码,要么需要遍历树并单独编译和执行每个子树,但我还没有完全弄清楚如何实现这一点。

有什么建议吗?

插入表达式树——如何获得每个子树的计算结果

虽然amon的帖子在理论上是正确的,但据我所知,C#ExpressionTrees没有解释器。然而,有一个编译器,还有一个很好的抽象访问者,可以很好地用于此目的。

public class Program
{
    static void Main(string[] args)
    {
        Expression<Func<int, bool>> x = (i => i > 3 && i % 4 == 0);
        var visitor = new GetSubExpressionVisitor();
        var visited = (Expression<Func<int, bool>>)visitor.Visit(x);
        var func = visited.Compile();
        var result = func(4);
    }
}
public class GetSubExpressionVisitor : ExpressionVisitor
{
    private readonly List<ParameterExpression> _parameters = new List<ParameterExpression>();
    protected override Expression VisitLambda<T>(Expression<T> node)
    {
        _parameters.AddRange(node.Parameters);
        return base.VisitLambda(node);
    }
    protected override Expression VisitBinary(BinaryExpression node)
    {
        switch (node.NodeType)
        {
            case ExpressionType.Modulo:
            case ExpressionType.Equal:
            case ExpressionType.GreaterThanOrEqual:
            case ExpressionType.LessThanOrEqual:
            case ExpressionType.NotEqual:
            case ExpressionType.GreaterThan:
            case ExpressionType.LessThan:
            case ExpressionType.And:
            case ExpressionType.AndAlso:
            case ExpressionType.Or:
            case ExpressionType.OrElse:
                return WithLog(node);
        }
        return base.VisitBinary(node);
    }
    public Expression WithLog(BinaryExpression exp)
    {
        return Expression.Block(
            Expression.Call(
                typeof(Debug).GetMethod("Print", new Type[] { typeof(string) }),
                new[] 
                { 
                    Expression.Call(
                        typeof(string).GetMethod("Format", new [] { typeof(string), typeof(object), typeof(object)}),
                        Expression.Constant("Executing Rule: {0} --> {1}"),
                        Expression.Call(Expression.Constant(exp), exp.GetType().GetMethod("ToString")),
                        Expression.Convert(
                            exp,
                            typeof(object)
                        )
                    )
                }
            ),
            base.VisitBinary(exp)
        );
    }
}

如果你有一个嵌套的lambda,我不完全确定这个代码的工作效果如何,但如果你没有这样的东西,这应该可以做到


合并了WithLog代码。代码输出以下内容:

Executing Rule: ((i > 3) AndAlso ((i % 4) == 0)) --> True
Executing Rule: (i > 3) --> True
Executing Rule: ((i % 4) == 0) --> True
Executing Rule: (i % 4) --> 0

不幸的是,我没有使用C#和表达式树的经验,但我对解释器略知一二。

我假设您的表达式树是一种AST,其中每个树节点都是公共层次结构中的一个类。我还假设您通过应用解释器模式(通过expr.Interpret(context)方法或ExpressionInterpreter访问者)来评估此AST。

使用Interpret()方法时

您将希望引入一种具有以下语义的新表达式类型LoggedExpression

  • 它包含一个表达式
  • 评估时,它评估子节点,并打印出字符串化的子节点和结果:
class LoggedExpression : Expression {
  private Expression child;
  public LoggedExpression(Expression child) { ... }
  public string ToString() { return child.ToString(); }
  public bool Interpret() {
    bool result = child.Interpret();
    log("Executing rule: " + child + " --> " + result);
    return result;
  }
}

如果你的语言比简单的布尔表达式更复杂,你会想在评估之前登录,这样你就可以很容易地调试挂机等。

然后,您必须将表达式树转换为已记录的表达式树。这可以很容易地通过方法AsLoggedExpression()来完成,该方法复制每个节点,但将其封装在日志表达式中:

class Or : Expression {
  private Expression left;
  private Expression right;
  ...
  public Expression AsLoggedExpression() {
    return new LoggedExpression(new Or(left.AsLoggedExpression(), right.AsLoggedExpression()));
  }
}

一些节点可能会返回自己不变的状态,而不是添加日志记录,例如常量或日志记录表达式本身(因此,将日志记录添加到树将是一个幂等运算)。

使用访问者时

访问者中的每个visit()方法都负责评估表达式树节点。给定您的主ExpressionInterpreter访问者,我们可以导出一个LoggingExpressionInterpreter,它为每个节点类型记录表达式并对其求值:

class LoggingExpressionInterpreter : ExpressionInterpreter {
  ...
  public bool Visit(Expression.Or ast) {
    bool result = base.Visit(ast);
    log("Executing rule: " + child + " --> " + result);
    return result;
  }
}

在这里,我们不能使用组合而不是继承,因为这会破坏递归日志记录。重要的是,在评估任何节点时,日志解释器也用于所有子节点。如果我们取消继承,Visit()AcceptVisitor()方法将需要访问者的显式参数,该参数应应用于子节点。

我更喜欢基于访问者的方法,因为它不必修改表达式树,而且代码总量更少(我想)。