运算式树(Expression tree)深入学习

news/2024/11/18 9:29:44/文章来源:https://www.cnblogs.com/soraofficial/p/18302981

前言

运算式树(Expression tree)是二叉树数据结构。
目的是实现方便的叠加各种查询条件,无限制的拼接成一个查询条件。提高复杂查询逻辑的编码效率。

一、Lambda表达式

Lambda表达式分为运算式Lambda和语句式Lambda
下面用两种lambda实现同样功能的委托。

(1)运算式Lambda(Expression lambda)

也翻译成陈述式lambda、表达式 lambda。

Func<int, int> 运算式Lambda = 
(t => t + 100);int number = 运算式Lambda(6);
//number = 106

(2)语句式Lambda(Statement lambda)

也翻译成语句 lambda。

Func<int, int> 语句式Lambda = 
t =>
{return t + 100;
};int number = 语句式Lambda(6);
//number = 106

运算式(Expression lambda)的主体为运算表达式,语句式(Statement lambda)的主体为语句块(特征是有大括号)。

运算式Lambda(Expression lambda)是可以被包装成运算式树(Expression tree)的。

二、运算式树(Expression tree)

运算式树可以理解为运算式组成的二叉树

Expression<Func<int, int>> lambdaExpression = (t => t + 100);

对应的二叉树为:

通过IDE快速监视,关注这个表达式树的几个主要属性

属性名称 含义
Body 整个树的表达式(展开后是根节点的属性)
NodeType 当前结点类型
Parameters 入参集合
ReturnType 返回值类型

(1)运算式树的结点类型(NodeType)

运算式树常见的结点类型:

结点类型 含义
Parameter 变量结点
Constant 常量结点
Add、Subtract 加法、减法等四则运算结点
And、Or 与、或等逻辑运算节点
Call 调用函数的节点

更多的结点类型:

https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.expressions.expressiontype?view=net-5.0

(2)分析表达式树对象

结合对应二叉树的图看:

根节点(Body)


NodeType:当前节点的类型为Add(加运算)
Type:数据类型(Int32)
Left:当前节点的左子树(展开后是左子树根节点属性)
Right:当前节点的右子树(展开后是右子树根节点属性)

节点的左子树(Left)

可以看到左子结节点的属性:

NodeType:当前节点的类型为“Parameter”(变量结点)。
Name:变量名称为"t"。
Type:数据类型为"Int32"。
该节点没有Left、Right,说明它是二叉树的叶子节点。

节点的右子树(Right)

可以看到右子结点的属性:

NodeType:当前节点的类型为“Constant”(常量结点)
Value:值为100。
数据类型为"Int32"。

安装ExpressionTreeVisualizer插件后看的更直观

https://github.com/zspitz/ExpressionTreeVisualizer/releases

三、自己拼装表达式树

(1)使用叶节点拼装(四则运算)

还以 t + 100 这个简单的加法运算举例,创建表达式树的代码如下:

//创建 t + 100 的表达式树
//创建变量节点t
ParameterExpression parax = Expression.Parameter(typeof(int), "t");
//创建常量节点100
ConstantExpression consty = Expression.Constant(100, typeof(int));
//创建lambda表达式树
LambdaExpression lambdaExp = Expression.Lambda(Expression.Add(parax,consty),new List<ParameterExpression>() { parax }
);
//将表达式树编译成委托再执行
var lambdaExpValue = lambdaExp.Compile().DynamicInvoke(1);
//lambdaExpValue = 101;

(2)使用叶节点拼装(逻辑运算)

实际应用中没有场景去用到运算表达式,都是拼装逻辑运算的表达式树,作为参数传给Where()方法。
创建一个学生IQueryable做模拟数据源

//学生类,属性有年龄和姓名
Stu stu1 = new Stu()
{Age = 10,Name = "曹操"
};
Stu stu2 = new Stu()
{Age = 20,Name = "刘备"
};
Stu stu3 = new Stu()
{Age = 20,Name = "孙策"
};
//学生IQueryable
IQueryable<Stu> StuQ= new List<Stu> { stu1, stu2, stu3 }.AsQueryable();

分别查询两个结果集。

List<Stu> StuListR1 = StuQ.Where(t => t.Age == 20).ToList();
List<Stu> StuListR2 = StuQ.Where(t => t.Name.Contains("孙")).ToList();

可以看到Where()扩展方法,参数类型是Expression<Func<Stu, bool>>

进一步分别将表达式树提取出来,获得两个Expression<Func<Stu, bool>>作为参数传递给Where()方法。
lambda1 和 lambda2 如下

Expression<Func<Stu, bool>> lambda1 = (t => t.Age == 20);
Expression<Func<Stu, bool>> lambda2 = (t => t.Name.Contains("孙"));List<Stu> StuListR1 = StuQ.Where(lambda1).ToList();
List<Stu> StuListR2 = StuQ.Where(lambda2).ToList();

如果我们要获得一个年龄为10岁并且姓名包含孙的查询结果。表达式树lambda3如下。
出现了很多新的节点类型,按照树形图捋一下

Expression<Func<Stu, bool>> lambda3 = (t => t.Age == 20 && t.Name.Contains("孙"));


(3)使用表达式树拼装

我们已经有了lambda1和lambda2,
接下来尝试,将它们拼装成同时满足两个条件的lambda3,就会遇到一个坑

            Expression<Func<Stu, bool>> lambda1 = (t => t.Age == 20);Expression<Func<Stu, bool>> lambda2 = (t => t.Name.Contains("孙"));Expression<Func<Stu, bool>> lambda3 = (Expression<Func<Stu, bool>>)Expression.Lambda(Expression.And(lambda1.Body,lambda2.Body),new List<ParameterExpression>() {Expression.Parameter(typeof(Stu))});//这句话会报错List<Stu> StuListR4 = StuQ.Where(lambda3).ToList();

这样拼接,将报错“变量t未定义”。
拼接Lambda的坑就是:lambda1和lambda2拼接后,这两表达式的变量即使同名也不会自动关联上。
编译器认为lambda1的变量t和lambda2的变量t其实是两个不相关的参数,最终生成的表达式应该是有两个参数。
(其实这里给的参数是lambda3的变量,和lambda1、lambda2的变量t都没关联上。)
正确的表达式树是:

            Expression<Func<Stu, Stu, bool>> lambda3 = (Expression<Func<Stu, Stu, bool>>)Expression.Lambda(Expression.And(lambda1.Body,lambda2.Body),new List<ParameterExpression>() {lambda1.Parameters[0],lambda2.Parameters[1]});

其类型是“Expression<Func<Stu, Stu, bool>>”(两个Stu),
和Where()函数需要的入参类型“Expression<Func<Stu, bool>>”(一个Stu)对不上。

我们希望获得Expression<Func<Stu, bool>>类型的lambda3,才成传递给Where()。

为了填上这个坑,需要进行节点替换操作。
让最终的表达树用同一个参数。(将lambda1和lambda2中的参数节点,都替换成我们赋给lambda3的参数节点)。

参考文档:

https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/expression-trees/how-to-modify-expression-trees

https://stackoverflow.com/questions/30556911/variable-of-type-referenced-from-scope-but-it-is-not-defined

四、封装节点替换与拼装表达式树的函数

作用:将两个表达式树合并成一个树,并替换所有参数节点为同一个参数。
输入:两个bool返回值的表达式树。
输出:拼接后的树。根节点类型是Add,返回值类型为bool。

/*-------------------------------------------------------------------------*      ___/>    フ|   _  _|/`  ミ_xノ/  -WuTian-|/  ヽ    ノ│  | |  |/ ̄|   | | || ( ̄ヽ__ヽ_)__)\二つ* 版本号:v1.0*  -------------------------------------------------------------------------*/public static class ExpressionExtension{/// <summary>/// Expression的泛型扩展(拼接表达式并替换参数)/// </summary>/// <typeparam name="TSource">泛型Expression</typeparam>/// <param name="a">源Expression</param>/// <param name="b">拼接的Expression</param>/// <returns></returns>public static Expression<Func<TSource, bool>> And<TSource>(this Expression<Func<TSource, bool>> a, Expression<Func<TSource, bool>> b){//建一个最终使用的参数节点ParameterExpression replacePara = Expression.Parameter(typeof(TSource), "myPara");var exprBody = Expression.And(a.Body, b.Body);exprBody = (BinaryExpression)new ParameterReplacer(replacePara).Visit(exprBody);return Expression.Lambda<Func<TSource, bool>>(exprBody, replacePara);}}/// <summary>/// 继承:ExpressionVisitor/// </summary>public class ParameterReplacer : ExpressionVisitor{private readonly ParameterExpression replacePara;internal ParameterReplacer(ParameterExpression _replacePara){replacePara = _replacePara;}protected override Expression VisitParameter(ParameterExpression expression){return base.VisitParameter(replacePara);}}

使用封装好的函数,用Lambda1、Lambda2去拼装Expression<Func<Stu, bool>>类型的Lambda3

            Expression<Func<Stu, bool>> lambda1 = (t => t.Age == 20);Expression<Func<Stu, bool>> lambda2 = (t => t.Name.Contains("孙"));Expression<Func<Stu, bool>> lambda3 = lambda1.And(lambda2);List<Stu> stuR = StuQ.Where(lambda3).ToList();

可以看到拼装后的表达式树中的参数节点都已经替换成了同一个参数(myPara):

到此为止已经成功执行查询到结果了:

更精简的写法:

            Expression<Func<Stu, bool>> lambdaExpression = (t => true);lambdaExpression = lambdaExpression.And(t => t.Age == 20);lambdaExpression = lambdaExpression.And(t => t.Name.Contains("孙"));List<Stu> stuR = StuQ.Where(lambdaExpression).ToList();

五、食用方式

实际开发中通过这种方法,将接口与业务层解耦。
接口负责只将查询条件拼成条件表达式树。业务层只负责执行查询,将涉及到的表的Iqueryable进行关联,投影(=>select)出DTO模型的字段,通过表达式树进行条件查询。

(1)直接使用封装的函数

现在有学生、学校两张表。
页面查询条件:
姓名、性别、学费范围
页面要显示:
姓名、年龄、性别、学校、学费

创建学生、学校两张表及对应的ORM模型:

using Chloe.Annotations;
namespace EasyCore.Entity.DB_Entity
{/// <summary>/// ORM模型:STU表/// </summary>[Table("STU")]public class Db_Stu{public int ID { get; set; }public string Name { get; set; }public int Age { get; set; }public string Gender { get; set; }public string School { get; set; }}
}
using Chloe.Annotations;
namespace EasyCore.Entity.DB_Entity
{/// <summary>/// ORM模型:SCHOOL表/// </summary>[Table("SCHOOL")]public class Db_School{public string School { get; set; }public decimal Price { get; set; }}
}

创建一个DTO模型:

namespace EasyCore.Model
{/// <summary>/// DTO模型/// </summary>public class Dto_StuPrice{public string Name { get; set; }public int Age { get; set; }public string Gender { get; set; }public decimal Price { get; set; }}
}

创建接口的参数模型:

    public class StuPriceParaModel{public string Name { get; set; }public string Gender { get; set; }public decimal? MaxPrice { get; set; }public decimal? MinPrice { get; set; }}

根据需求开始编写接口及业务层:
接口:

        public ActionResult SearchStuPrice(StuPriceParaModel paraModel){//使用参数创建条件表达式树Expression<Func<Dto_StuPrice, bool>> lambda = (t => true);if (paraModel.Gender != null)lambda = lambda.And(a => a.Gender == paraModel.Gender);if (paraModel.Name != null)lambda = lambda.And(a => a.Name.Contains(paraModel.Name));if (paraModel.MaxPrice != null)lambda = lambda.And(a => a.Price <= paraModel.MaxPrice);if (paraModel.MinPrice != null)lambda = lambda.And(a => a.Price >= paraModel.MinPrice);//调用业务层,把条件表达式树作为参数传进去List<Dto_StuPrice> dto_StuPrices = demoService.SreachStuPrice(lambda);//返回数据return JsonResult(dto_StuPrices);}

业务层:只负责获得拼装好的lambdaExpression执行查询,返回查询结果

        public List<Dto_StuPrice> SreachStuPrice(Expression<Func<Dto_StuPrice, bool>> lambda){//两张表的IQueryableIQuery<Db_Stu> dB_StuQ = DbContext.Query<Db_Stu>();IQuery<Db_School> dB_School = DbContext.Query<Db_School>();//创建DTO模型的IQueryableIQuery<Dto_StuPrice> dto_StuPriceQ =dB_StuQ.LeftJoin(dB_School, (x, y) => x.School == y.School).Select((x, y) => new Dto_StuPrice{Name = x.Name,Age = x.Age,Gender = x.Gender,Price = y.Price});//用条件表达式树,做条件查询dto_StuPriceQ = dto_StuPriceQ.Where(lambda);//延迟查询List<Dto_StuPrice> dto_StuList = dto_StuPriceQ.ToList();return dto_StuList;}

这样一来,将接口与业务层解除耦合。
对于查询条件的修改,只需要修改接口,不需要去动其他代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/744070.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

htmlToPdf处理视频

一个写好的html页面要打印pdf,其中有视频也有图片。参考了网上的一些方法,最终是在获取数据的时候,对视频进行了截取第一帧处理。 getFirstImgBase64(){this.piclist.forEach(item => {if(item.url.endsWith(.mp4)) {let dataURL = ""let video = document.cre…

my-tv修复版本

github上很火的项目,但是作者已不再维护,这里分享一个修复版本,可以正常观看 软件链接:https://pan.quark.cn/s/836a5050fcab

提升Camstar性能

😘宝子:除非不再醒来,除非太阳不再升起,不然都请你好好生活,挣扎着前进,开心的笑。(●◡●)

K8S教程:如何使用Kubeadm命令在PetaExpress Ubuntu系统上安装Kubernetes集群

Kubernetes,通常缩写为K8s,是一个开源的容器编排平台,旨在自动化容器化应用的部署、扩展和管理。有了Kubernetes,您可以轻松地部署、更新和扩展应用,而无需担心底层基础设施。一个Kubernetes集群由控制平面节点(master节点)和工作节点(worker节点)组成。确保集群的高效运行…

Panda数据处理

一、 Pandas简介 Pandas,python+data+analysis的组合缩写,是python中基于numpy和matplotlib第三方数据分析库,与后者共同构成python数据分析基础工具包,享有数据三剑客之名。正因为pandas是在numpy基础上实现的,其核心数据结构与numpy的ndarray十分相似,但pandas与numpy的…

1.基础知识

单片机:Single-Chip Microcomputer,单片微型计算机,是一种集成电路芯片 CPU:CPU内存:SRAM硬盘:FLASH主板:外设 仪器仪表:电源/示波器/焊台家用电器:空调/冰箱/洗衣机工业控制:机器人/PLC/电梯汽车电子:GPS/ABS/胎压监测 :数据与程序储存在同一存储器,分时复用Core…

线程池遇到父子任务,有大坑,要注意!

你好呀,我是歪歪。 最近在使用线程池的时候踩了一个坑,给你分享一下。 在实际业务场景下,涉及到业务代码和不同的微服务,导致问题有点难以定位,但是最终分析出原因之后,发现可以用一个很简单的例子来演示。 所以歪师傅这次先用 Demo 说问题,再说场景,方便吸收。Demo 老…

线性规划模型复习总结

线性规划(Linear Programming, LP)是一种数学优化方法,用于在给定约束条件下最大化或最小化目标函数。线性规划广泛应用于经济、工程、管理等领域,通过建立数学模型,帮助决策者找到最优解决方案。 一、线性规划数学模型 1.1 模型三要素目标函数(Objective Function) 目标…

nginx ./nginx -s reload 工作进程pid与实际不一致导致平滑重启报错

某次修改nginx配置后使用 ./nginx -s reload 重启nginx, 结果报错, 如图所示, 去kill工作进程的时候显示找不到该进程原因是位于nginx/logs下的 nginx.pid 中的pid和实际不一致造成的 nginx.pid 文件是 Nginx web 服务器在启动时创建的一个进程标识符 (PID) 文件。这个文件包含…

第七天学习笔记(经验测试,白盒测试)

经验测试法 错误推测法 基于经验的测试技术之错误推测法 错误推测法也叫错误猜测法,就是根据经验猜想,已有的缺陷,测试经验和失败数据等可能有什么问题并依此设计测试用例.异常分析法 基于经验的测试技术之异常分析法 系统异常分析法就是针对系统有可能存在的异常操作、软硬…

SQL Server sa账号被锁定

SQL Server的sa账号老是被锁(登录已锁定)问题:最近sa登录老是被锁定,去查看数据库->安全性->sa账号右键属性,在状态选项卡中找到sql server 身份验证 ,发现“登录已锁定”被勾选了;把这个勾选去掉,然后提示必须修改密码。“登录已锁定”的勾选去掉并修改密码后又能…