c#:表达式树概念及应用场景(Expression)

环境:

  • window 10
  • vs2019 16.5.1
  • .netcore 3.1
  • .Net Reflector 10
  • ILSpy版本6.0.0.5559-preview2

参考:

目的:
探索什么是表达式树?
表达式能用来做什么?
表达式树在Entity Framework中起到了什么作用?

一、什么是表达式树

在c#中,我们可以定义一种树状的数据结构来描述c#中的代码,这种树状的数据结构就是表达式树,也称之为表达式(各种表达式之间是可以相互嵌套的)。比如说:(5-2)+(2+3)这个表达式,拆分成树状结构如下图:
在这里插入图片描述
当然,c#代码肯定比这个要复杂的多,比如:你的定义语句、循环、判断、属性访问等等。。。
在c#中,微软为每种运算类型的代码定义了不同的表达式类型,它们有共同的基类:Expression。

表达式树基类 Expression(抽象类)

说明:它表示所有出现在c#中的代码的类型,主要包含两个属性:NodeType和Type。
它的定义如下图:
在这里插入图片描述

说下Expression的两个属性:

  • NodeType:这个表达式的节点类型,这是一个枚举,c#中定义了85个节点类型
    在这里插入图片描述
  • Type:这个表达式的静态类型,也就是这个表达式的返回类型

二、常用的表达式类型有哪些?

我们直接看有哪些表达式类是继承自Expression的:
在这里插入图片描述
当然还有没有直接继承Expression的,比如:Expression<T>(我们最常用的表达式树)的继承关系为:Expression<T>=>LambdaExpression=>Expression=>Object,这里我就挑选几个表达式树简单描述一下:

  • BinaryExpression:二元运算表达式
      1). Right:二元运算符的右侧表达式(Expression)
      2). Left:二元运算符的左侧表达式(Expression)
  • ConstantExpression:常量表达式
      1). Value:常量值,Object
  • ConditionalExpression:条件表达式
      1). IfFalse:为False时的表达式(Expression)
      2). IfTrue:为True时的表达式(Expression)
      3). Test:判断表达式
  • ParameterExpression:参数表达式
      1). IsByRef:参数是否传引用
      2). Name:参数名称
  • LambdaExpression:lambda表达式
      1).Body:内容表达式(Expression)
      2).Parameters:参数表达式集合(ReadOnlyCollection)
      3).ReturnType:返回的类型
      4).Compile():方法,编译生成委托
  • Expression<T>:带有泛型的表达式,一般这个T就是委托类型的,继承自LabelExpression

三、怎么创建表达式树

3.1 纯手工组装

3.1.1 将简单lambda表达式:(x,y)=>x+y用表达式树的形式表示出来
static void Main(string[] args)
{
     //组装表达式树
     ParameterExpression para1 = Expression.Parameter(typeof(int), "x");
     ParameterExpression para2 = Expression.Parameter(typeof(int), "y");
     var paras = new ParameterExpression[] { para1, para2 };
     BinaryExpression body = Expression.Add(para1, para2);
     var expression = Expression.Lambda(body, paras);
     //编译表达式树
     var func = expression.Compile() as Func<int, int, int>;
     //调用执行
     var res = func(1, 2);
     Console.WriteLine($"表达式树的执行结果:(1,2)=>{res}");
     Console.WriteLine("Hello World!");
     Console.ReadLine();
 }

运行如下:
在这里插入图片描述

3.1.2 将复杂的方法定义用表达式树的形式表示出来

原方法定义:

public static string GetScoreDesc(string name, int score, int standard)
{
    string desc = "";
    if (score > standard)
    {
        desc = "已及格";
    }
    else if (score == standard)
    {
        desc = "刚及格";
    }
    else
    {
        desc = "未及格";
    }
    return name + desc;
}

用表达式树表示如下:

private static void Test()
{
	//定义三个参数: string name,ing score,int standard
    ParameterExpression paraName = Expression.Parameter(typeof(string), "name");
    ParameterExpression paraScore = Expression.Parameter(typeof(int), "score");
    ParameterExpression paraStandard = Expression.Parameter(typeof(int), "standard");
    //定义一个局部变量:string desc;
    ParameterExpression defineDesc = Expression.Parameter(typeof(string), "desc");
    //给局部变量赋值:desc="";
    BinaryExpression assignDesc = Expression.Assign(defineDesc, Expression.Constant(""));
    //准备赋值语句:desc="已及格"
    BinaryExpression assignDescYijige = Expression.Assign(defineDesc, Expression.Constant("已及格"));
    //准备赋值语句:desc="刚及格"
    BinaryExpression assignDescGangjige = Expression.Assign(defineDesc, Expression.Constant("刚及格"));
    //准备赋值语句:desc="未及格"
    BinaryExpression assignDescWeijige = Expression.Assign(defineDesc, Expression.Constant("未及格"));
    //准备代码:score>standard
    BinaryExpression greaterThan = Expression.MakeBinary(ExpressionType.GreaterThan, paraScore, paraStandard);
    //准备代码:score==standard
    BinaryExpression equalThan = Expression.MakeBinary(ExpressionType.Equal, paraScore, paraStandard);
    //准备代码:score<standard
    BinaryExpression lessThan = Expression.MakeBinary(ExpressionType.LessThan, paraScore, paraStandard);
    //组装判断逻辑块:if(score>standard){desc="已及格"}else{if(score==standard){desc="刚及格"}else{desc="未及格"}}
    ConditionalExpression conditional = Expression.Condition(greaterThan, assignDescYijige, Expression.Condition(equalThan, assignDescGangjige, assignDescWeijige));
    //准备代码:name+desc (注意:methodof运算符是用c#自定义的,代码在后面)
    BinaryExpression addAssign = Expression.Add(paraName, defineDesc, (methodof<Func<string, string, string>>)(String.Concat));
    //定义标记:这个标记用于方法返回值
    LabelTarget labelTarget = Expression.Label(typeof(string));
    //定义返回代码段:默认返回 desc
    LabelExpression labelExpression = Expression.Label(labelTarget, defineDesc);
    //定义return语句(这里是goto语句):goto labelTarget 
    GotoExpression gotoExpression = Expression.Return(labelTarget, addAssign, typeof(string));
    //组装这个方法的方法体(指定局部变量)
    BlockExpression block = Expression.Block(typeof(string), new ParameterExpression[] { defineDesc }, assignDesc, conditional, gotoExpression, labelExpression);
    //完成组装这个方法(指定参数)
    LambdaExpression expression = Expression.Lambda<Func<string, int, int, string>>(block, new ParameterExpression[] { paraName, paraScore, paraStandard });

	//编译测试...
    var delega = expression.Compile();
    var func = delega as Func<string, int, int, string>;
    var res = func("xiaoming", 5, 6);
    Console.WriteLine($"func(\"xiaoming\", 5, 6)=>{func("xiaoming", 5, 6)}");
    Console.WriteLine($"func(\"xiaoming\", 2, 6)=>{func("xiaoming", 2, 6)}");
    Console.WriteLine($"func(\"xiaoming\", 6, 6)=>{func("xiaoming", 6, 6)}");
    Console.WriteLine($"func(\"xiaoming\", 8, 6)=>{func("xiaoming", 8, 6)}");
	
	//尝试打印出表达式树的字符串形式    
    Console.WriteLine("整体输出...");
    Console.WriteLine(expression.ToString());
    Console.WriteLine("--------------------------start");
    Console.WriteLine(assignDesc.ToString());
    Console.WriteLine(conditional);
    Console.WriteLine(gotoExpression);
    Console.WriteLine(labelExpression);
    Console.WriteLine("---------------------------end");
}

上面用到的methodof运算符定义如下:

public class methodof<T>
{
    private MethodInfo method;

    public methodof(T func)
    {
        Delegate del = (Delegate)(object)func;
        this.method = del.Method;
    }

    public static implicit operator methodof<T>(T methodof)
    {
        return new methodof<T>(methodof);
    }

    public static implicit operator MethodInfo(methodof<T> methodof)
    {
        return methodof.method;
    }
}

整个代码输出如下:
在这里插入图片描述
从上面可以看到表达式树不仅可以组装简单的lambda表达式,还可以带语句块的复杂方法。

3.1.3 应用示例

我们可以利用表达式树完成AutoMapper类似的功能,下面的示例展示如何将Person动态转换成PersonDto,这在批量转换时性能是很高的:

// 模型定义:(PersonDto除了和Person的类名不同,其他的一样)
public enum EnumSex
{
    Male, FeMale
}
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Addr { get; set; }
    public string IdentityNo { get; set; }
    public EnumSex Sex { get; set; }
    public DateTime Birth { get; set; }
    public string Description { get; set; }
    public int Age { get; set; }
    public string Nick { get; set; }
    public string QQ { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public string WeiXin { get; set; }
    public string Picture { get; set; }
    public string Intro { get; set; }
    public double Score { get; set; }
    public List<string> Books { get; set; }
}
//获取委托
private static Func<TFrom, TDest> getMapperFunc<TFrom, TDest>()
{
    var para = Expression.Parameter(typeof(TFrom), "src");

    //var res=new TDest();
    var variable = Expression.Variable(typeof(TDest), "res");
    var newExp = Expression.New(typeof(TDest).GetConstructor(new Type[0]));
    var assign = Expression.Assign(variable, newExp);

    // 属性赋值
    var assignProps = new List<BinaryExpression>();
    var props = typeof(TFrom).GetProperties();
    var destProps = typeof(TDest).GetProperties();
    foreach (var prop in destProps)
    {
        if (prop.CanWrite)
        {
            var srcProp = props.FirstOrDefault(i => i.Name == prop.Name);
            if (srcProp.Name == prop.Name)
            {
                //属性
                var assignProp = Expression.Assign(Expression.Property(variable, prop.Name), Expression.MakeMemberAccess(para, srcProp));
                assignProps.Add(assignProp);
            }
        }
    }

    //return res;
    var labelTarget = Expression.Label(typeof(TDest));
    var labelExpression = Expression.Label(labelTarget, variable);
    var gotoExpression = Expression.Return(labelTarget, variable, typeof(TDest));

    var expressions = new List<Expression>();
    expressions.Add(assign);
    expressions.AddRange(assignProps);
    expressions.Add(gotoExpression);
    expressions.Add(labelExpression);
    //组装表达式块,编译生成委托
    var block = Expression.Block(typeof(TDest), new ParameterExpression[] { variable }, expressions);
    var mapperFunc = Expression.Lambda<Func<TFrom, TDest>>(block, para).Compile();
    return mapperFunc;
}

public static void Test()
{
	//定义tmpList集合,里面有1万个Person实体
	var person = new Person(){Id=1};
	PersonDto dto = null;
	var func = getMapperFunc<Person, PersonDto>();
	var count = 50;
    st4 = new Stopwatch();
    st4.Start();
    for (var i = 0; i < count; i++)
    {
     	var st2 = new Stopwatch();
        st2.Start();
        for (var j = 0; j < 10000; j++)
        {
            dto = func(person);
        }
        st2.Stop();
        Console.WriteLine($"第{(i+1)}次: {st2.ElapsedMilliseconds}毫秒");
    }
    st4.Stop();
    Console.WriteLine($"表达式树 总耗时: {st4.ElapsedMilliseconds}毫秒 平均: {st4.ElapsedMilliseconds / (count + 0.0)}毫秒");
}

3.2 使用编译器的语法糖

同样是上面的lambda表达式`:(x,y)=>x+y,代码如下:

static void Main(string[] args)
{
    //组装表达式树
    Expression<Func<int, int, int>> expression = (x, y) => x + y;
    //编译并调用
    var res = expression.Compile()(1, 2);
    Console.WriteLine($"表达式树的执行结果:(1,2)=>{res}");
    Console.WriteLine("Hello World!");
    Console.ReadLine();
}

毫无疑问,运行效果是一样的:
在这里插入图片描述
我们用.Net Reflector反编译看一下:
在这里插入图片描述
可以看到,这就是个语法糖。我们在entity framework中经常会用到这种语法糖,看下图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
从图中可以看到,entity framework中的参数传递用的都是表达式树(而不是我们熟悉的委托。。。),但一般我们穿进去的是lambda表达式,这里也是语法糖:编译器会在编译的时候将你写的委托组装成表达式树再传进入:
在这里插入图片描述
注意:
这个语法糖有一个限制:就是你的lambda代码不能有代码块(而这个特点正好被用在了entity framework上防止你任意的输入无限量代码)!
看下图:
在这里插入图片描述

四、表达式树能做什么?

上面讲到了,手动组装表达式树然后编译调用,现实中肯定不能这么用,如果是自己组装表达式再自己调用的话,为什么不干脆直接调用委托?
所以说,表达式树是给框架的作者用的。试想一下:当你调用框架方法的时候,你会传入一个简单的lambda表达式(lambda表达式还是很方便的),然后框架拿到你传入的表达式树(编译器编译时就已经将你的lambda表达式改装成了表达式树)并进行分析结构,然后做一些东西(对于entity framework来说就是分析表达式树 -> 生成适当sql语句->执行sql语句)。

下一篇:《c#:深入理解表达式树》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jackletter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值