Expression Tree 上手指南 (二)
上回我们说到Expression Tree是一种表示编程语言中“表达式”概念的树状数据结构,并且学习了从Lambda表达式自动生成表达式树的C#语法。那么它到底有什么用呢?其实上一回已经提到了Expression Tree的基本功能:分析表达式的逻辑、保存和传输表达式以及重新编译表达式。现在我们就分别来看这三项基本功能如何使用。
分析表达式的逻辑
表达式树中已经包含了表达式所需的各种成分,我们只需要像遍历树一样遍历表达式树,就可以解析表达式的含义。我们现在就来动手解析一下四则运算的表达式树。假设表达式中仅有加减乘除运算。比方说我们有这个一个表达式(a, b, m, n) => m * a * a + n * b * b,它的表达式树是这样:
最外层是LambdaExpression,我们很容易就可以从它的Body属性得到里面的表达式。在限定条件下,这个表达式只含有BinaryExpression一种表达式。我们只需要要遍历这棵树,在遇到BinaryExpression的时候完成所需的运算即可。我们此处采用的是后序遍历的逻辑:
class Program {static void Main(string[] args){Expression<Func<double, double, double, double, double>> myExp =(a, b, m, n) => m * a * a + n * b * b;var calc = new BinaryExpressionCalculator(myExp);Console.WriteLine(calc.Calculate(1, 2, 3, 4));} }class BinaryExpressionCalculator {Dictionary<ParameterExpression, double> m_argDict;LambdaExpression m_exp;public BinaryExpressionCalculator(LambdaExpression exp){m_exp = exp;}public double Calculate(params double[] args){//从ExpressionExpression中提取参数,和传输的实参对应起来。//生成的字典可以方便我们在后面查询参数的值m_argDict = new Dictionary<ParameterExpression, double>();for (int i = 0; i < m_exp.Parameters.Count; i++){//就不检查数目和类型了,大家理解哈m_argDict[m_exp.Parameters[i]] = args[i];}//提取树根Expression rootExp = m_exp.Body as Expression;//计算!return InternalCalc(rootExp);}double InternalCalc(Expression exp){ConstantExpression cexp = exp as ConstantExpression;if (cexp != null) return (double)cexp.Value;ParameterExpression pexp = exp as ParameterExpression;if (pexp != null){return m_argDict[pexp];}BinaryExpression bexp = exp as BinaryExpression;if (bexp == null) throw new ArgumentException("不支持表达式的类型", "exp");switch (bexp.NodeType){case ExpressionType.Add:return InternalCalc(bexp.Left) + InternalCalc(bexp.Right);case ExpressionType.Divide:return InternalCalc(bexp.Left) / InternalCalc(bexp.Right);case ExpressionType.Multiply:return InternalCalc(bexp.Left) * InternalCalc(bexp.Right);case ExpressionType.Subtract:return InternalCalc(bexp.Left) - InternalCalc(bexp.Right);default:throw new ArgumentException("不支持表达式的类型", "exp");}} } |
我们用了一个递归逻辑实现了遍历,为了方便封装了一个简单的类。这个简单的程序就可以计算任意double型参数的四则运算表达式。大家可能要问了,C#本身就具有运算四则运算表达式的能力,为什么我们要亲自解析表达式树来做到的同样的功能呢?如果仅仅是计算表达式的值,当然不用自己来解析。但自己解析的一大好处就是可以在解析过程中加入自定义的语义动作。比如我们只要稍微更改一下上述程序中的InternalCalc就能输出Lambda表达式的前缀序列:
string InternalPrefix(Expression exp) {ConstantExpression cexp = exp as ConstantExpression;if (cexp != null) return cexp.Value.ToString();ParameterExpression pexp = exp as ParameterExpression;if (pexp != null) return pexp.Name;BinaryExpression bexp = exp as BinaryExpression;if (bexp == null) throw new ArgumentException("不支持表达式的类型", "exp");switch (bexp.NodeType){case ExpressionType.Add:return "+ " + InternalPrefix(bexp.Left) + " " + InternalPrefix(bexp.Right);case ExpressionType.Divide:return "- " + InternalPrefix(bexp.Left) + " " + InternalPrefix(bexp.Right);case ExpressionType.Multiply:return "* " + InternalPrefix(bexp.Left) + " " + InternalPrefix(bexp.Right);case ExpressionType.Subtract:return "/ " + InternalPrefix(bexp.Left) + " " + InternalPrefix(bexp.Right);default:throw new ArgumentException("不支持表达式的类型", "exp");} } |
您可以尝试一下将自己的四则运算表达式转换成前缀序列是什么样子,有点函数式语言的味道了~ 我们还可以改变上述程序,让他实时输出四则运算的中间结果等。大家可以试验一下。
由此可见,我们分析表达式树并不一定是要计算表达式的值,我们可以在翻译表达式的时候执行任何自定义的语义动作,达到翻译表达式或用表达式驱动特殊代码的作用。Linq to Sql就是这个用途的典型例子。我们知道Linq to Sql的QueryProvider可以将IQueryable上的一系列查询动作翻译成SQL语句。那么表达式树是如何充当这个角色的呢?下面我们来看一个Queryable的例子:
IQueryable<double> source = var query = from d in source where d > 0 select d * 2; |
这个查询语句等价于如下直接使用Queryable扩展方法的代码:
var query = source.Where(d => d > 0).Select(d => d * 2); |
和Enumerable类型的扩展方法不同,Queryable扩展方法的每个操作都接受一个强类型的LambdaExpression参数。因此上述代码中的Where和Select分别接受了一个Expression Tree作为参数。最后的query对象中包含了整个序列每个操作对应的表达式树,QueryProvider就可以通过分析表达式树,翻译成SQL或者直接进行查询动作了。
这种用法有点类似于解释器模式。你可以用Expression Tree来表示一种逻辑,然后解析它来驱动你的业务逻辑。原本属于具体语言的表达式现在成为了我们可以直接利用的一大利器。
序列化和传输表达式
通过上文我们学习了解析表达式树并且执行自定义动作的方法。在实际应用中,我们还可能遇到以下需求:
1. 生成表达式的程序与解析表达式的程序不在同一进程内(例如在客户端生成表达式,在服务端解析)。
2. 需要储存或缓存表达式,为以后多次使用做准备。
3. 需要用其他非.NET技术处理或生成表达式树。
以上需求需要将表达式树当成纯数据来看待。内存中保存表达式树固然没有问题,我们还需要探究表达式树序列化的问题。
.NET 3.5版本的表达式树本身并没有特别优雅的序列化方式,它并不支持DataContract序列化。因为表达式的种类繁多,许多表达式都和CLR具体类型相关。通常的做法是,根据自己要使用的表达式子集手动编写序列化的代码。ToString()也可以得到一个表示表达式的字符串,可惜他不是那么容易变回表达式树。所以ToString()通常仅仅作为显示和参考。
MSDN Code Gallery中有一个LuckH提供的通用Expression Tree序列化例子。它可以提供比较清晰地XML序列化结果。
关于Expression Tree缓存,可以看老赵的这一系列文章,你可以学到各种利用表达式树自身特性的有趣用法。
习题
1. 基于本文提供的四则运算例子,给他加上支持一元正负运算符和函数调用的功能。这样你就能计算Math.Sin(a) + b这样的表达式了。可以先仅支持静态函数。
2. 改写这个程序,使之能根据四则运算表达式树生成javascript代码。
3. 你可以试着将以上代码扩展成一个小小的类库,能用javascript来执行你提供的四则运算表达式。
(待续)
Expression Tree 上手指南 (二)相关推荐
- 快速上手Expression Tree(一):做一做装配脑袋的Expression Tree 习题
装配脑袋的习题在这里:Expression Tree上手指南 (一) 不了解Expression Tree的同学可以去看下,很好,很强大. 1: -a 2: a + b * 2 我把这些问题都弄成了方 ...
- Eclipse快速上手指南
本指南介绍到的软件可能已经有更新,希望大家不要局限于本文中的版本号 Eclipse是一款非常优秀的开源IDE,非常适合Java开发,由于支持插件技术,受到了越来越多的开发者的欢迎.最新的Eclipse ...
- 英雄探长的机器人怎么拼_LOL路人局都畏惧的辅助英雄,新版机器人布里兹上手指南...
最近国服版本更新到9.9对这个所谓的机器人做了不小的改动,算是一波不错的加强,在此推出一篇上手指南,希望能帮助大家上分. 版本更新如下: 解析:坦度下滑,伤害提升,大招被动机制不在干扰我方ADC补刀, ...
- 三种属性操作性能比较:PropertyInfo + Expression Tree + Delegate.CreateDelegate
在<上篇>中,我比较了三种属性操作的性能:直接操作,单纯通过PropertyInfo反射和IL Emit.本篇继续讨论这个话题,我们再引入另外两种额外的属性操作方式:Expression ...
- 一起谈.NET技术,关于Expression Tree和IL Emit的所谓的quot;性能差别quot;
昨天写了<三种属性操作性能比较>,有个网友写信问我一个问题:从性能上看,Expression Tree和IL Emit孰优孰劣?虽然我在回信中作了简单的回答,但不知道这个网友是否懂我的意思 ...
- 【Xilinx】Spartan 7上手指南(ARTY S7开发板)
Spartan 7上手指南 一.安装board文件 1. 下载并解压板卡压缩文件 2. 复制到Vivado安装目录 二.demo工程 1. 下载demo 2. 修改tcl 3. 恢复工程 4.生成bi ...
- latex 参考文献显示问号_UESTC 本科Latex毕设论文模板 无痛上手指南
置顶:如果大家觉得这篇文章对毕设有帮助,也欢迎转发给更多的朋友~注明出处即可~ ----更新 5.19---- 写在自己用latex完成毕业论文,并已经通过学校的查重系统之后(第一次查重11.7%是因 ...
- [ISUX译]iOS 9人机界面指南(二):设计策略
[ISUX译]iOS 9人机界面指南(二):设计策略 雪糕 2015.11.09 文章索引 2.1 设计原则(Design Principles) 2.1.1 美学完整性(Aesthetic Inte ...
- Android原生UI开发框架 《Jetpack Compose入门到精通》最全上手指南
前言 在去年的Google/IO大会上,亮相了一个全新的 Android 原生 UI 开发框架-Jetpack Compose, 与苹果的SwiftIUI一样,Jetpack Compose是一个声明 ...
最新文章
- workerman源码分析之启动过程
- 科技部颁布十大国家新一代人工智能开放创新平台,华为旷视等入选
- phpMyAdmin安全配置
- C++paranthesis matching括号匹配的算法(附完整源码)
- 企业进销存管理系统 email_进销存财务软件选它就对了!
- kafka基本管理操作命令
- Netflix数据库架构变革:缩放时间序列的数据存储
- 剑客决斗(NYOJ 110)
- html上传文件_.NET基于WebUploader大文件分片上传、断网续传、秒传
- 前端基础-html-段落标签
- 【今日CV 视觉论文速览】28 Nov 2018
- 进阶之路:Java 日志框架全画传(上)
- linux与电脑ping通配置方法
- 如何删除PDF中指定的一页或几页?
- pat 乙级 1094
- HTML制作个人名片
- 谷歌公布13GB 3D扫描数据集:17大类、1030个家用物品
- android studio 成长历程
- Beego exper表达式
- java赵云主角兵器谱游戏_见过最牛的三国杀兵器谱专题论述
热门文章
- confluence mysql 中文乱码_解决confluence的乱码问题
- VScode 无法创建文件或者无法保存文件
- el表达式 java_java基础学习:JavaWeb之EL表达式
- mamp python mysql_Python,MySQL,MAMP怎么做?
- Tomcat(三):tomcat处理连接的详细过程
- 备战2022秋季“金三银四”跳槽必备:软件测试面试题,贡献给需要的小伙伴,最后有惊喜哦
- 递增的整数序列链表的插入_每日算法题 | 剑指offer 链表专题 (5)链表中倒数第k个节点...
- linux redis ruby,redisrequiresrubyversion2.2.2的解决方案
- java中用于选择按钮的语句_java程序员考试套题1
- 输入一行字符,判断单词数