Instrumenting an expression tree -- 如何得到每个子树的计算结果?

Instrumenting an expression tree -- How to get the computed result of each subtree?

我正在做一些关于表达式树的工作,这是一种规则引擎。

当您在表达式树上调用 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 遍历树并向每个节点添加一些代码,要么遍历树并分别编译和执行每个子树,但我还没有完全弄清楚如何进行这项工作还没有。

有什么建议吗?

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

我假设你的表达式树是一种 AST,其中每个树节点都是一个公共层次结构中的 class。我还假设您通过 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()));
  }
}

有些节点可能 return 本身没有改变,而不是添加日志记录,例如常量或日志表达式本身(因此,将日志记录添加到树将是幂等操作)。

使用访问者时

访问者中的每个 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() 方法将需要一个明确的访问者参数,该参数应该应用于子节点。

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

虽然 amon 的 post 在理论上是正确的,但没有用于 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