本篇将介绍如何访问表达式树中的每个节点,同时生成该表达式树的已修改副本。 以下是在两个重要方案中将使用的技巧。 第一种是了解表达式树表示的算法,以便可以将其转换到另一个环境中。 第二种是何时更改已创建的算法。 这可能是为了添加日志记录、拦截方法调用并跟踪它们,或其他目的。

转换即访问

  生成的用于转换表达式树的代码是你已看到的用于访问树中所有节点的代码的扩展。 转换表达式树时,会访问所有节点,并在访问它们的同时生成新树。 新树可包含对原始节点的引用或已放置在树中的新节点。

让我们通过访问表达式树,并创建具有一些替换节点的新树,来查看其工作原理。 在此示例中,我们将任何常数替换为其十倍大的常数。  我们通过将常数节点替换为执行乘法运算的新节点来进行此替换,而不必阅读常数的值并将其替换为新的常数。

此处,在找到常数节点后,创建新乘法节点(其子节点是原始常数和常数 10):

  1. private static Expression ReplaceNodes(Expression original)
  2. {
  3. if (original.NodeType == ExpressionType.Constant)
  4. {
  5. return Expression.Multiply(original, Expression.Constant());
  6. }
  7. else if (original.NodeType == ExpressionType.Add)
  8. {
  9. var binaryExpression = (BinaryExpression)original;
  10. return Expression.Add(ReplaceNodes(binaryExpression.Left),
  11. ReplaceNodes(binaryExpression.Right));
  12. }
  13. return original;
  14. }

通过替换原始节点,将形成一个包含修改的新树。 可以通过编译并执行替换的树对此进行验证。

  1. var one = Expression.Constant(, typeof(int));
  2. var two = Expression.Constant(, typeof(int));
  3. var addition = Expression.Add(one, two);
  4. var sum = ReplaceNodes(addition);
  5. var executableFunc = Expression.Lambda(sum);
  6.  
  7. var func = (Func<int>)executableFunc.Compile();
  8. var answer = func();
  9. Console.WriteLine(answer);

生成新树是两者的结合:访问现有树中的节点,和创建新节点并将其插入树中。

此示例演示了表达式树不可变这一点的重要性。 请注意,上面创建的新树混合了新创建的节点和现有树中的节点。 这是安全的,因为现有树中的节点无法进行修改。 这可以极大提高内存效率。 相同的节点可能会在整个树或多个表达式树中遍历使用。 由于不能修改节点,因此可以在需要时随时重用相同的节点。

遍历并执行加法
  通过生成遍历加法节点的树并计算结果的第二个访问者来对此进行验证。 可以通过对目前见到的访问者进行一些修改来执行此操作。 在此新版本中,访问者将返回到目前为止加法运算的部分总和。 对于常数表达式,该总和即为常数表达式的值。 对于加法表达式,遍历这些树后,其结果为左操作数和右操作数的总和。

  1. var one = Expression.Constant(, typeof(int));
  2. var two = Expression.Constant(, typeof(int));
  3. var three= Expression.Constant(, typeof(int));
  4. var four = Expression.Constant(, typeof(int));
  5. var addition = Expression.Add(one, two);
  6. var add2 = Expression.Add(three, four);
  7. var sum = Expression.Add(addition, add2);
  8.  
  9. // 声明委托,这样就可以从它本身递归地调用它
  10. Func<Expression, int> aggregate = null;
  11. // 聚合、返回常量或左、右操作数之和。
  12. // 主要简化:假设每个二进制表达式都是一个加法。
  13. aggregate = (exp) => exp.NodeType == ExpressionType.Constant
    ? (int)((ConstantExpression)exp).Value
    : aggregate(((BinaryExpression)exp).Left) + aggregate(((BinaryExpression)exp).Right);
  14. var theSum = aggregate(sum);
  15. Console.WriteLine(theSum);

此处有相当多的代码,但这些概念是非常容易理解的。 此代码访问首次深度搜索后的子级。 当它遇到常数节点时,访问者将返回该常数的值。 访问者访问这两个子级之后,这些子级将计算出为该子树计算的总和。 加法节点现在可以计算其总和。 在访问了表达式树中的所有节点后,将计算出总和。 可以通过在调试器中运行示例并跟踪执行来跟踪执行。

让我们通过遍历树,来更轻松地跟踪如何分析节点以及如何计算总和。 下面是包含大量跟踪信息的聚合方法的更新版本:

  1. private static int Aggregate(Expression exp)
  2. {
  3. if (exp.NodeType == ExpressionType.Constant)
  4. {
  5. var constantExp = (ConstantExpression)exp;
  6. Console.Error.WriteLine($"Found Constant: {constantExp.Value}");
  7. return (int)constantExp.Value;
  8. }
  9. else if (exp.NodeType == ExpressionType.Add)
  10. {
  11. var addExp = (BinaryExpression)exp;
  12. Console.Error.WriteLine("Found Addition Expression");
  13. Console.Error.WriteLine("Computing Left node");
  14. var leftOperand = Aggregate(addExp.Left);
  15. Console.Error.WriteLine($"Left is: {leftOperand}");
  16. Console.Error.WriteLine("Computing Right node");
  17. var rightOperand = Aggregate(addExp.Right);
  18. Console.Error.WriteLine($"Right is: {rightOperand}");
  19. var sum = leftOperand + rightOperand;
  20. Console.Error.WriteLine($"Computed sum: {sum}");
  21. return sum;
  22. }
  23. else throw new NotSupportedException("Haven't written this yet");
  24. }

在同一表达式中运行该版本将生成以下输出:

  1. Found Addition Expression
  2. Computing Left node
  3. Found Addition Expression
  4. Computing Left node
  5. Found Constant:
  6. Left is:
  7. Computing Right node
  8. Found Constant:
  9. Right is:
  10. Computed sum:
  11. Left is:
  12. Computing Right node
  13. Found Addition Expression
  14. Computing Left node
  15. Found Constant:
  16. Left is:
  17. Computing Right node
  18. Found Constant:
  19. Right is:
  20. Computed sum:
  21. Right is:
  22. Computed sum:

跟踪输出,并在上面的代码中跟随。 应当能够看出代码如何在遍历树的同时访问代码和计算总和,并得出总和。

现在,让我们来看看另一个运行,其表达式由 sum1 给出:

  1. Expression<Func<int> sum1 = () => + ( + ( + ));

下面是通过检查此表达式得到的输出:

  1. Found Addition Expression
  2. Computing Left node
  3. Found Constant:
  4. Left is:
  5. Computing Right node
  6. Found Addition Expression
  7. Computing Left node
  8. Found Constant:
  9. Left is:
  10. Computing Right node
  11. Found Addition Expression
  12. Computing Left node
  13. Found Constant:
  14. Left is:
  15. Computing Right node
  16. Found Constant:
  17. Right is:
  18. Computed sum:
  19. Right is:
  20. Computed sum:
  21. Right is:
  22. Computed sum:

虽然最终结果是相同的,但树遍历完全不同。 节点的访问顺序不同,因为树是以首先发生的不同运算构造的。

限制

存在一些不好翻译成表达式树的较新的 C# 语言元素。 表达式树不能包含 await 表达式或 async lambda 表达式。 C# 6 发行中添加的许多功能不会完全按照表达式树中所编写的那样显示。 较新的功能可能会显示在表达式树中等效、早期的语法中。 这可能不像你想象的那样有局限性。 实际上,这意味着在引入新语言功能时,解释表达式树的代码将仍可能照常运行。

即使具有这些限制,通过表达式树,仍可创建依赖于解释和修改表示为数据结构的代码的动态算法。 它是一种功能强大的工具,作为 .NET 生态系统的一种功能,它可使丰富的库(如实体框架)完成其所执行的操作。

C#3.0新增功能10 表达式树 07 翻译(转换)表达式的更多相关文章

  1. C#3.0新增功能10 表达式树 01 简介

    连载目录    [已更新最新开发文章,点击查看详细] 如果你使用过 LINQ,则会有丰富库(其中 Func 类型是 API 集的一部分)的经验. (如果尚不熟悉 LINQ,建议阅读 LINQ 教程,以 ...

  2. C#3.0新增功能10 表达式树 06 生成表达式

    连载目录    [已更新最新开发文章,点击查看详细] 到目前为止,你所看到的所有表达式树都是由 C# 编译器创建的. 你所要做的是创建一个 lambda 表达式,将其分配给一个类型为 Expressi ...

  3. C#3.0新增功能10 表达式树 05 解释表达式

    连载目录    [已更新最新开发文章,点击查看详细] 表达式树中的每个节点将是派生自 Expression 的类的对象. 该设计使得访问表达式树中的所有节点成为相对直接的递归操作. 常规策略是从根节点 ...

  4. C#3.0新增功能10 表达式树 02 说明

    连载目录    [已更新最新开发文章,点击查看详细] 表达式树是定义代码的数据结构. 它们基于编译器用于分析代码和生成已编译输出的相同结构.表达式树和 Roslyn API 中用于生成分析器和 Cod ...

  5. C#3.0新增功能10 表达式树 03 支持表达式树的框架类型

    连载目录    [已更新最新开发文章,点击查看详细] 存在可与表达式树配合使用的 .NET Core framework 中的类的大型列表. 可以在 System.Linq.Expressions 查 ...

  6. C#3.0新增功能10 表达式树 04 执行表达式

    连载目录    [已更新最新开发文章,点击查看详细] 表达式树 是表示一些代码的数据结构. 它不是已编译且可执行的代码. 如果想要执行由表达式树表示的 .NET 代码,则必须将其转换为可执行的 IL ...

  7. C#3.0新增功能09 LINQ 基础07 LINQ 中的查询语法和方法语法

    连载目录    [已更新最新开发文章,点击查看详细] 介绍性的语言集成查询 (LINQ) 文档中的大多数查询是使用 LINQ 声明性查询语法编写的.但是在编译代码时,查询语法必须转换为针对 .NET ...

  8. C#3.0新增功能08 Lambda 表达式

    连载目录    [已更新最新开发文章,点击查看详细] Lambda 表达式是作为对象处理的代码块(表达式或语句块). 它可作为参数传递给方法,也可通过方法调用返回. Lambda 表达式广泛用于: 将 ...

  9. C#基础拾遗系列之二:使用ILSpy探索C#7.0新增功能点

    C#基础拾遗系列之二:使用ILSpy探索C#7.0新增功能点   第一部分: C#是一种通用的,类型安全的,面向对象的编程语言.有如下特点: (1)面向对象:c# 是面向对象的范例的一个丰富实现, 它 ...

随机推荐

  1. 青云QingCloud黄允松:最高效的研发管理就是没有管理

    摘要: 对于底层技术创新而言,没有管理是最好的管理,小规模作战,快速试错,迅速转变方向,迭代周期一定要短. 钛媒体注:钛媒体.商业价值联合主办的第五届“MIIC移动互联网创新大会”如期举行.2015 ...

  2. Delphi中取得汉字的首字母(十分巧妙)

    function Tdm.GetHzPy(const AHzStr: string): string;const  ChinaCode: array[0..25, 0..1] of Integer = ...

  3. VS使用的快捷方式

    VS常用快捷键 1.回到上一个光标位置/前进到下一个光标位置 1)回到上一个光标位置:使用组合键“Ctrl + -”: 2)前进到下一个光标位置:“Ctrl + Shift + - ”. 2.复制/剪 ...

  4. Java算法-求最大和的子数组序列

    问题:有一个连续数组,长度是确定的,它包含多个子数组,子数组中的内容必须是原数组内容中的一个连续片段,长度不唯一,子数组中每个元素相加的结果称为子数组的和,现要求找出和最大的一个子数组. 具体算法如下 ...

  5. 很多程序员都没搞明白的时间与时区知识 - 24时区/GMT/UTC/DST/CST/ISO8601

    全球24个时区的划分      相较于两地时间表,可以显示世界各时区时间和地名的世界时区表(World Time),就显得精密与复杂多了,通常世界时区表的表盘上会标示着全球24个时区的城市名称,但究竟 ...

  6. MacOS平台上编译 hadoop 3.1.2 源码

    1. 先从官方下载源码:源码下载地址:https://hadoop.apache.org/releases.html,下载 3.1.2 版本 2. 解压缩源码:tar xvf hadoop-3.1.2 ...

  7. 深入理解Java虚拟机(字节码执行引擎)

    深入理解Java虚拟机(字节码执行引擎) 本文首发于微信公众号:BaronTalk 执行引擎是 Java 虚拟机最核心的组成部分之一.「虚拟机」是相对于「物理机」的概念,这两种机器都有代码执行的能力, ...

  8. 使用Visual Studio Code进行MicroPython编程

    转载请注明文章来源,更多教程可自助参考docs.tpyboard.com,QQ技术交流群:157816561,公众号:MicroPython玩家汇 Visual Studio Code(以下简称VSC ...

  9. 点菜网---Java开源生鲜电商平台-技术选型(源码可下载)

    点菜网---Java开源生鲜电商平台-技术选型(源码可下载) 1.内容简介 点菜网目前选用的是最流行的微服务架构模式,采用前后端分离的开发模式,具备高可用,高负载,支持千万级别的数据量的请求. 2. ...

  10. kubernetes实战篇之Dashboard的访问权限限制

    系列目录 前面我们的示例中,我们创建的ServiceAccount是与cluster-admin 绑定的,这个用户默认有最高的权限,实际生产环境中,往往需要对不同运维人员赋预不同的权限.而根据实际情况 ...