本篇将介绍如何访问表达式树中的每一个节点,同时生成该表达式树的已修改副本。 如下是在两个重要方案中将使用的技巧。 第一种是了解表达式树表示的算法,以即可以将其转换到另外一个环境中。 第二种是什么时候更改已建立的算法。 这多是为了添加日志记录、拦截方法调用并跟踪它们,或其余目的。html
生成的用于转换表达式树的代码是你已看到的用于访问树中全部节点的代码的扩展。 转换表达式树时,会访问全部节点,并在访问它们的同时生成新树。 新树可包含对原始节点的引用或已放置在树中的新节点。node
让咱们经过访问表达式树,并建立具备一些替换节点的新树,来查看其工做原理。 在此示例中,咱们将任何常数替换为其十倍大的常数。 咱们经过将常数节点替换为执行乘法运算的新节点来进行此替换,而没必要阅读常数的值并将其替换为新的常数。算法
此处,在找到常数节点后,建立新乘法节点(其子节点是原始常数和常数 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);
生成新树是二者的结合:访问现有树中的节点,和建立新节点并将其插入树中。框架
此示例演示了表达式树不可变这一点的重要性。 请注意,上面建立的新树混合了新建立的节点和现有树中的节点。 这是安全的,由于现有树中的节点没法进行修改。 这能够极大提升内存效率。 相同的节点可能会在整个树或多个表达式树中遍历使用。 因为不能修改节点,所以能够在须要时随时重用相同的节点。async
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); // 声明委托,这样就能够从它自己递归地调用它 Func<Expression, int> aggregate = null; // 聚合、返回常量或左、右操做数之和。 // 主要简化:假设每一个二进制表达式都是一个加法。 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);
此处有至关多的代码,但这些概念是很是容易理解的。 此代码访问首次深度搜索后的子级。 当它遇到常数节点时,访问者将返回该常数的值。 访问者访问这两个子级以后,这些子级将计算出为该子树计算的总和。 加法节点如今能够计算其总和。 在访问了表达式树中的全部节点后,将计算出总和。 能够经过在调试器中运行示例并跟踪执行来跟踪执行。工具
让咱们经过遍历树,来更轻松地跟踪如何分析节点以及如何计算总和。 下面是包含大量跟踪信息的聚合方法的更新版本:this
private static int Aggregate(Expression exp) { if (exp.NodeType == ExpressionType.Constant) { var constantExp = (ConstantExpression)exp; Console.Error.WriteLine($"Found Constant: {constantExp.Value}"); return (int)constantExp.Value; } 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"); }
在同一表达式中运行该版本将生成如下输出:spa
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
虽然最终结果是相同的,但树遍历彻底不一样。 节点的访问顺序不一样,由于树是以首先发生的不一样运算构造的。
存在一些很差翻译成表达式树的较新的 C# 语言元素。 表达式树不能包含 await
表达式或 async
lambda 表达式。 C# 6 发行中添加的许多功能不会彻底按照表达式树中所编写的那样显示。 较新的功能可能会显示在表达式树中等效、早期的语法中。 这可能不像你想象的那样有局限性。 实际上,这意味着在引入新语言功能时,解释表达式树的代码将仍可能照常运行。
即便具备这些限制,经过表达式树,仍可建立依赖于解释和修改表示为数据结构的代码的动态算法。 它是一种功能强大的工具,做为 .NET 生态系统的一种功能,它可以使丰富的库(如实体框架)完成其所执行的操做。