插入表达式树——如何获得每个子树的计算结果
本文关键字:计算 结果 何获得 表达式 插入 | 更新日期: 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()
方法将需要访问者的显式参数,该参数应应用于子节点。
我更喜欢基于访问者的方法,因为它不必修改表达式树,而且代码总量更少(我想)。