首页 > 其他分享 >运算式树(Expression tree)深入学习

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

时间:2024-07-15 13:40:46浏览次数:9  
标签:运算 tree public Name Expression 节点 表达式 lambda

前言

运算式树(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)
        {
            //两张表的IQueryable
            IQuery<Db_Stu> dB_StuQ = DbContext.Query<Db_Stu>();
            IQuery<Db_School> dB_School = DbContext.Query<Db_School>();

            //创建DTO模型的IQueryable
            IQuery<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;
        }

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

标签:运算,tree,public,Name,Expression,节点,表达式,lambda
From: https://www.cnblogs.com/soraofficial/p/18302981

相关文章

  • 指针的运算
    目录开头1.什么是指针的运算?总结指针±\pm±整数指针......
  • Solution - Codeforces 1311E Construct the Binary Tree
    先去考虑找一下无解条件。首先就是有\(d\)关于\(n\)的下界\(L\),就是弄成一颗完全二叉树的答案。其次有\(d\)关于\(n\)的上界\(R\),就是成一条链的样子。首先当\(d<L\)或\(R<d\)时显然无解。对于\(L\led\leR\)又如何去判定。能发现没有一个比较好的判定......
  • C语言 底层逻辑详细阐述指针(一)万字讲解 #指针是什么? #指针和指针类型 #指针的解引用 #
    文章目录前言序1:什么是内存?序2:地址是怎么产生的?一、指针是什么1、指针变量的创建及其意义:2、指针变量的大小二、指针的解引用 三、指针类型存在的意义四、野指针1、什么是野指针2、野指针的成因a、指针未初始化b、指针越界访问c、指针指向的空间释放3、如何......
  • 【python基础】常见的运算符
    一、常见的逻辑运算符1、逻辑“与”运算符---andand对符号两侧的值进行与运算,只有两侧均为True时候最终结果才为True,与运算主要找False,如果第一个值为False,则不在运算第二个值>>>print(0and1)0>>>aa=1andprint('23')23>>>aa=0andprint('23')>>>>>>......
  • 运算符重载
    运算符重载是什么:重新赋予运算符新含义,添加参数或创建,允许在程序中定义或修改运算符的行为类似函数一样。重载位置:在类中写相当于举例:要实现两个向量相加structVector2{ floatx,y; Vector2(floatx,floaty)//初始化结构体变量 :x(x),y(y) { } Vector2Add(cons......
  • TreeMap
    TreeMap由红黑树实现,可以保持元素的自然顺序,或者实现了Comparator接口的自定义顺序红黑树(英语:Red–blacktree)是一种自平衡的二叉查找树(BinarySearchTree),结构复杂,但却有着良好的性能,完成查找、插入和删除的时间复杂度均为log(n)。自然顺序默认情况下,TreeMap是根据ke......
  • C++ //练习 14.44 编写一个简单的桌面计算器使其能处理二元运算。
    C++Primer(第5版)练习14.44练习14.44编写一个简单的桌面计算器使其能处理二元运算。环境:LinuxUbuntu(云服务器)工具:vim 代码块/************************************************************************* >FileName:ex14.44.cpp >Author: >Mail: >C......
  • 05day--C++日期类的实现与取地址运算符的重载
    这里写目录标题5.3⽇期类实现6.取地址运算符重载6.1const成员函数6.2取地址运算符重载5.3⽇期类实现Date.h#pragmaonce#include<iostream>usingnamespacestd;#include<assert.h>classDate{//友元函数声明friendostream&operator<<(ostream&......
  • 运算符的关系,什么叫一元运算符,二元运算符,三元运算符,运算符优先级,以及运算符的
    按照操作数个数区分:一元运算符:一元运算符只需要一个操作数。常见的一元运算符有:1.递增和递减运算符:++和--,用于对操作数进行增加或减少1。2.正负号运算符:+和-,用于表示正负数。3.逻辑非运算符:!,用于对布尔值进行取反。二元运算符:二元运算符需要两个操作数。常见的二元运......
  • 三元运算符 栈 堆 隐式转换 笔记
    是什么:相当于if语句的语法糖代码示例:std::stringrank=level>10?"Master":"Begining";判断条件?为真保留:为假保留;可以嵌套使用,最好别用看的头疼;栈通常非常小通常为1兆2兆;浅要提及堆上飞陪比栈花费更多时间,而且要手动释放内存若对象太大或要显式地控制对象的生存期,就在堆......