【问题标题】:Instrumenting an expression tree -- How to get the computed result of each subtree?检测表达式树——如何获取每个子树的计算结果?
【发布时间】:2016-01-05 06:22:49
【问题描述】:

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

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

有什么建议吗?

【问题讨论】:

  • 您需要使用表达式访问者。您需要编译作为 lambda 表达式的节点。然后,您可以将编译后的 lambda 作为委托执行。如果没记错的话,每个 lambda 节点上都有一个 compile 方法,它会返回一个委托。您可以执行委托以获得结果。

标签: c# logging expression-trees instrumentation


【解决方案1】:

虽然 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# 中的一个特定事物,并且它们是经过编译的。这可能意味着您必须创建一个新的表达式树来添加跟踪。您显示的代码已经完成了一半,但实际上并没有导致记录执行。相反,它似乎在评估某些选定的子表达式。
  • 哦,我明白他想做什么了。我会相应地更新。表达式树是 C# 中的一个东西,从 C#3.0/.NET 3.5 开始。典型案例类似于objectSource.Where(o =&gt; o.Id &gt; 10) 或类似的东西。如果objectSource 是一个SQL 表,您可以将表达式编译成SELECT * FROM Objects WHERE id &gt; 10) 之类的东西。如果它是内存中的集合,您将使用 C# 编译器。
  • 哇,这是一个非常酷的语言功能。顺便说一句,我知道没有其他语言(除了 Lisp、Perl6、Mathematica 等)具有类似的功能,尽管它可以通过运算符重载进行部分模拟。感谢您向我解释这一点!看来我应该再次检查一下 Linux 上 C# 的状态。
  • 并不是说它与问题真正相关,但 CoreFX 包含表达式的编译器和解释器。要使用解释器,请调用 Compile(true) 而不是 Compile()
  • 这仅对BinaryExpression 嵌入日志记录。我会使用ExpressionTreeToString 来创建表达式树的字符串表示,并为所有表达式类型记录它。 (免责声明:我是该库的作者。)
【解决方案2】:

不幸的是,我没有 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() 方法将需要一个显式的访问者参数,该参数应该应用于子节点。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多