栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

《c#进阶》--04.表达式树

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

《c#进阶》--04.表达式树

文章目录
  • 一、基本概念
  • 二.基本使用
    • 初识表达式树
    • 原生语法使用
      • 运算符基本使用
      • 面向对象的基本使用
  • 三、ExpressionVisitor
    • 简介
    • 入门体验
    • 案例一
    • 案例二
  • 四、Expression语法糖应用
    • 表达式的拼接
    • 自定义扩展方法
    • 深拷贝
      • 类型已知
      • 类型未知

提示:以下是本篇文章正文内容,下面案例可供参考

一、基本概念
  • 委托是表达式目录树的一个参数类型
  • 表达式树是用于描述一个方法的方式,必须调用Combile编译后才能运行
  • Expression 编译–Compile–得到一个委托
  • Expression只能有一行代码一委托一方法一可以有多行
  • 形似于二叉树的一个数据结构
二.基本使用 初识表达式树

两个功能一摸一样的语法,不同的是,委托可以直接使用,表达式树需要在编译后才能使用

public void test01()
{
    //1.委托
    Func a = () => "hello world" ;
    string str1 = a();
    Console.WriteLine(str1);

    //2.表达式树
    Expression> b = () => "hello world";
    //Compile编译后才能使用
    string str2 = b.Compile().Invoke();
    Console.WriteLine(str2);
}

查看反编译生成的代码,使用c#2.0可以查看到最终编译时生成的语法,比想象中复杂的多

原生语法使用 运算符基本使用

使用原生的语法去编写表达式树,了解底层的操作原理,是如何一步步拼接起来的,对我们后续的应用非常有帮助

1)案例一
无参返回值

public void test02()
{
    Func a = () => 5;
    //使用Expression工具类去描述我们的方法
    //1.先描述参数,参数没有可以不描述
    //2.描述方法体

    //2.1 此处方法体中只有一个常量()
    ConstantExpression con = Expression.Constant(5);
    //2.2 方法的结构是lambda,传入方法体的内容,以及lambda的参数
    Expression.Lambda>(con,null);
}

2)案例二
有参返回值

public void test03()
{
    Func a = (para) => 5+para;

    //1.先描述参数,参数的类型|参数名称
    ParameterExpression pa = Expression.Parameter(typeof(int), "para");
    //2.描述方法体
    //2.1 一个常量5
    ConstantExpression con = Expression.Constant(5);
    //2.2 一个变量,这个变量其实就是参数,不需要再写
    //2.3描述加法 5+pa
    BinaryExpression ba = Expression.Add(con,pa);
    //2.4 描述方法结构 pa=>5+pa
    Expression> expression = Expression.Lambda>(ba,new ParameterExpression[] {pa});
    int result = expression.Compile().Invoke(3);
    Console.WriteLine(result);
}

3)案例三
复杂表达式

 public void test04()
{
    Func func = (a, b) => a + a * b - 3;
    //1.先描述方法参数
    ParameterExpression pa1 = Expression.Parameter(typeof(int), "a");
    ParameterExpression pa2 = Expression.Parameter(typeof(int), "b");
    //2.描述方法体
    //2.1 描述常量3
    ConstantExpression con = Expression.Constant(3);

    //2.2先描述乘法
    BinaryExpression mul = Expression.Multiply(pa1, pa2);
    //2.3 描述加法 a+(a*b)
    BinaryExpression b1 = Expression.Add(pa1,mul);
    //2.4 描述加法 (a+a*b)-3
    BinaryExpression body = Expression.Subtract(b1,con);
    //2.5 描述整体结构
    Expression> expression = 
                            Expression.Lambda>(body, new ParameterExpression[] {pa1,pa2});
    int v = expression.Compile().Invoke(1,2);
    Console.WriteLine(v);
}
面向对象的基本使用
class Person
{
    public string name { get; set; }
    public int id { get; set; }
}
------------------------------------------------------------------------
public void test05()
{
    Func func=p=>p.id.ToString();
    //1.描述参数
    ParameterExpression pa1 = Expression.Parameter(typeof(Person), "p");
    //2.描述方法体内容 p.id
    MemberExpression pro = Expression.Property(pa1, "id");

    //3.描述调用方法 p.id.ToString();
    MethodCallExpression body = Expression.Call(pro, "toString", null);

    //4.方法结构的声明
    Expression> expression = 
                            Expression.Lambda>(body,new ParameterExpression[] {pa1});
    string result = expression.Compile().Invoke(new Person() { id=32});
    Console.WriteLine(result);

}
三、ExpressionVisitor

当我们使用Expression的语法糖,去写简单的语法时,实际编译时会生成大量上面尝试写过的代码,这个过程就是底层ExpressionVisitor的解析过程。

简介
  • 一个逆向工程,来拆解析Expression语法糖的过程,也是可以操作细节,留下来专门给程序员使用的后门
  • 系统提供的有抽象类–就是需要我们来扩展一不能直接用
  • Visit方法是解析Expreesion入口—判断当前是什么类型的表达目录树,去调用更加专业的解析方法去接续解析~~ 类型多-每个类型都对应一个方法
  • 记住一句话,二元操作是核心内容,所有的表达式过程,都会逐步的拆解成一级级的二元操作,也就是VisitBinary方法
入门体验

继承此类,实现几个其中的方法

public class MyVisitor:ExpressionVisitor
{
    /// 
    /// 重写一下解析入口方法
    /// 
    /// 
    /// 
    public override Expression Visit(Expression node)
    {
        Console.WriteLine("进入到重写方法Visit");
        return base.Visit(node);
    }

    /// 
    /// 重写一下二元操作过程
    /// 
    /// 
    /// 
    protected override Expression VisitBinary(BinaryExpression node)
    {
        Console.WriteLine("进入到重写方法VisitBinary");
        return base.VisitBinary(node);
    }

    /// 
    /// 重写一下逻辑运算
    /// 
    /// 
    /// 
    protected override Expression VisitConditional(ConditionalExpression node)
    {
        Console.WriteLine("进入到重写方法VisitConditional");
        return base.VisitConditional(node);
    }
}
------------------------------------------------------------------
Expression> lam = p => p.name == "xzp" && p.id>23;
MyVisitor myVisitor = new MyVisitor();
myVisitor.Visit(lam);
------------------------------------------------------

结果如下:

进入到重写方法Visit
进入到重写方法Visit
进入到重写方法VisitBinary
进入到重写方法Visit
进入到重写方法VisitBinary
进入到重写方法Visit
进入到重写方法Visit
进入到重写方法Visit
进入到重写方法Visit
进入到重写方法VisitBinary
进入到重写方法Visit
进入到重写方法Visit
进入到重写方法Visit
进入到重写方法Visit

可以看出Visit方法会被多次返回又重新进行分析,递归式的分发到下面的指定方法

案例一

如果我们写的表达式为>,在重写的过程中把他换成<,如何实现?

//目标解析的Expression对象
Expression> lam = p => p.name == "xzp" && p.id>23;
-------------------------------------------------------------------------
//首先可以定位到,>出现在二元操作上,那我们可以在此方法上做手脚即可
/// 
/// 重写一下二元操作过程
/// 
/// 
/// 
protected override Expression VisitBinary(BinaryExpression node)
{
    //当前二元表达式的值类型
    Type type1 = node.Type;
    //当前二元表达式的操作符
    ExpressionType nodeType = node.NodeType;

    //当前二元表达式的左边
    Expression left = node.Left;
    //当前二元表达式的右边
    Expression right = node.Right;

    //在此处不能直接改node的属性,因为它是只读的,
    //直接返回Expression让这个过程重新解析即可
    if (nodeType == ExpressionType.GreaterThan)
    {
        return Expression.LessThan(left, right);
    }
    Console.WriteLine("进入到重写方法VisitBinary");
    return base.VisitBinary(node);
}
案例二

模拟linq to sql的过程,把筛选条件变成sql语句。
先看linq to sql的生成语句

Expression> func = p => p.name == "xzp" && p.id > 12;
IQueryable sql = db.student.Where(func);
Console.WriteLine(sql);

结果:
SELECt
    [Extent1].[id] AS [id],
    [Extent1].[classId] AS [classId],
    [Extent1].[levelId] AS [levelId],
    [Extent1].[name] AS [name],
    [Extent1].[age] AS [age],
    [Extent1].[sex] AS [sex],
    [Extent1].[classLevel] AS [classLevel],
    [Extent1].[classNum] AS [classNum]
    FROM [dbo].[student] AS [Extent1]
    WHERe ('xzp' = [Extent1].[name]) AND ([Extent1].[id] > 12)

明显换成了SQL语句,此处仅仅将条件筛选换成SQL的模式

//一个扩展方法,方便后面的使用
public static  class MyType
{
    public static string TypeToSql(this ExpressionType ex)
    {
        string result = "";
        switch (ex)
        {
            case ExpressionType.And:
            case ExpressionType.AndAlso:
                result = "and";              
                break;
            case ExpressionType.Equal:
                result = "=";
                break;
            case ExpressionType.GreaterThan:
                result = ">";
                break;
            case ExpressionType.GreaterThanOrEqual:
                result = ">=";
                break;
            case ExpressionType.LessThan:
                result = "<";
                break;
            case ExpressionType.LessThanOrEqual:
                result = "<=";
                break;   
            default:
                break;
        }
        return result;
    }
}
------------------------------------------------------------------------
public class MyVisitor:ExpressionVisitor
{
    public List sql = new List();

    /// 
    /// 重写一下二元操作过程,此处非常重要!!!
    /// 默认的解析顺序
    ///     1.操作符
    ///     2.left表达式
    ///     3,right表达式
    /// 默认的顺序对我们的拼接是非常不利的!!!需要手动的去指定顺序
    /// 
    /// 
    /// 
    protected override Expression VisitBinary(BinaryExpression node)
    {
        //1.手动解析左边
        this.Visit(node.Left);
        //2.操作符
        sql.Add(" "+node.NodeType.TypeToSql()+" ");
        //3.解析右边
        this.Visit(node.Right);
        //4.返回node,此处已经手动指定了解析顺序,不需要调父类VisitBinary
        return node;
    }
    //p => p.name == "xzp" && p.id 
    protected override Expression VisitMember(MemberExpression node)
    {
        //把属性名称添加进去,防止和sql关键重合,可以加[]
        sql.Add($"[{node.Member.Name}]");
        return base.VisitMember(node);
    }

    protected override Expression VisitConstant(ConstantExpression node)
    {
        //值用单引号包起来
        sql.Add($"'{node.Value.ToString()}'");
        return base.VisitConstant(node);
    }

    public string getSql()
    {
        return String.Concat(sql);
    }
}
四、Expression语法糖应用

此处总结Expression在描述方法时的重要几个步骤:

  • 声明参数(没有参数类型可以不声明)
  • 声明执行的内容(方法体,即时他只能有一行表达式)
  • 声明方法的结构
表达式的拼接

下面的两个表达式看上去似乎很好拼接

Expression> func1 = p => p.name == "xzp";
Expression> func2 = p => p.id >12;

1)使用Expression的静态方法add,返回返回值类型并不是Expression的泛型类型,而是一个二元操作

Expression> func1 = p => p.name == "xzp";
Expression> func2 = p => p.id >12;

BinaryExpression binaryExpression = Expression.And(func1, func2);

2)执行就会报错,别妄想强行转换成Expression类型,然后就可以执行,这是为什么呢?

  • 在上面的原生写法中,我们总结了重要的几个步骤,从步骤就能看出问题:
  • 拼接后参数是谁?参数一致吗?怎么声明一致?
  • 拼接后的是新方法的描述吗?为什么没有看到方法结构描述的内容?

3)总结,Expression方法看似是帮我们做了第二步,描述了方法体,实际上并不准确,我们需要And的是方法体,而不是整个方法的And。正确方式如下

Expression> func1 = p => p.name == "xzp";
Expression> func2 = p => p.id > 12;


//1.声明参数,两个一样,随便取一个即可,转换为数组是因为最后声明方法结构时需要参数数组
ParameterExpression[] pas = func1.Parameters.ToArray();
//2.取出各自的方法体
Expression body1 = func1.Body;
Expression body2 = func2.Body;
//3.拼接成一个方法体
BinaryExpression body = Expression.And(body1, body2);
//4.声明方法的结构
Expression> expression = Expression.Lambda>(body, pas);
自定义扩展方法

通过上面的例子,我们可以进行自定义扩展方法的拼接,来处理这种同参数的Expression的拼接。

public static class ExpressionTool
{
    public static Expression> And(this Expression> ex1,
                                                  Expression> ex2)
    {
        if(ex1!=null && ex2 != null)
        {
            ParameterExpression[] pas = ex1.Parameters.ToArray();
            BinaryExpression body = Expression.And(ex1.Body, ex2.Body);
            return Expression.Lambda>(body, pas);
        }
        return null;
    }

    public static Expression> Or(this Expression> ex1,
                                                  Expression> ex2)
    {
        if (ex1 != null && ex2 != null)
        {
            ParameterExpression[] pas = ex1.Parameters.ToArray();
            BinaryExpression body = Expression.Or(ex1.Body, ex2.Body);
            return Expression.Lambda>(body, pas);
        }
        return null;
    }
    public static Expression> Not(this Expression> ex1)
    {
        if (ex1 != null)
        {
            ParameterExpression[] pas = ex1.Parameters.ToArray();
            UnaryExpression body = Expression.Not(ex1.Body);
            return Expression.Lambda>(body, pas);
        }
        return null;
    }
}
----------------------------------------------------------------
//测试
Expression> func1 = p => p.name == "xzp";
Expression> func2 = p => p.id > 12;
Expression> func3 = p => p.id <23;
Expression> func4 = func1.And(func2).Not().And(func3);
深拷贝

1)深拷贝在面向对象中就是通过赋值的方式依次将属性重新的赋值,浅拷贝即引用的赋值,浅拷贝的危害在于拷贝对象成员的属性等的变化会影响到原对象,因为引用同一个对象。
2)在某些应用场景中,两个不同类型的对象之间,属性可能是相同的,这时候可能需要一次属性的赋值,类似于深拷贝的方式。

两个类型如下:

public class Person
{
    public string Name { get; set; }
    public string userName { get; set; }
    public int Id { get; set; }
    public int age { get; set; }
}

--------------------------------------------------------------------------
public class PersonA
{
    public string Name { get; set; }
    public int Id { get; set; }
    public int age { get; set; }
}
-------------------------------------------------------------------------
//一个已知的对象
Person person = new Person()
{
    age=12,
    Id=1201,
    Name="xzp",
    userName="admin",
};
类型已知
//如果是已知类型可以这么做(直接编码),性能也是最优的,如果数量较大,也是非常快的
PersonA personA = new PersonA()
{
    age=person.age,
    Name=person.Name,
    Id=person.Id,
};
类型未知

如果类型未知,或者想写一个通用得到方法,通常想到反射,但是在数据量大时,性能会比较差,写法如下

public static T deepCopy(V v) where T:class where V : class
{
    Type typev = typeof(V);
    Type typet = typeof(T);

    //1.创建T类型的对象并返回
    T t = typet.GetConstructor(new Type[] { }).Invoke(new object[] { }) as T;

    PropertyInfo[] propertyInfos = typev.GetProperties();
    //2.遍历V的属性,获取到属性名
    foreach (var pro in propertyInfos)
    {
        string name = pro.Name;
        if(typet.GetProperty(name) != null)
        {
            //3.给v设置值
            typet.GetProperty(name).SetValue(t, pro.GetValue(v, null), null);
        }
    }
    return t;
}

表达式树实现,效率非常高,可以进行几万次遍历进行测试执行的时间,注意返回方法委托,不要每次都去动态生成方法,不然性能可能更差了,此处利用了泛型委托的缓存机制,动态编译生成的类存入缓存,多次执行无需重新加载

public static Func deepCopy1(V v) where T : class where V : class
{
    
    //参照上面的结构进行!!!描述这个过程即最优解
    Type typev = typeof(V);
    Type typet = typeof(T);

    //1.声明参数,就是{}里对象即参数
    ParameterExpression pa = Expression.Parameter(typev, "person");

    //2.声明一个属性绑定集合,也就是age=person.age,这种语法,存储起来
    List bs = new List();

    PropertyInfo[] propertyInfos = typev.GetProperties();

    foreach (var pro in propertyInfos)
    {
        string name = pro.Name;
        if (typet.GetProperty(name) != null)
        {
            //3.person.xxx
            MemberExpression me = Expression.Property(pa, pro);
            //4.xxx = person.xxx
            MemberAssignment me2 = Expression.Bind(typet.GetProperty(name),me);
            bs.Add(me2);
        }
    }
    //5.new PersonA()
    //{
    //    age = person.age,
    //        Name = person.Name,
    //        Id = person.Id,
    //};
    MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typet),bs);
    Expression> ex = Expression.Lambda>(memberInitExpression, pa);
    return ex.Compile();
}

对比:

Stopwatch w1 = new Stopwatch();
Stopwatch w2 = new Stopwatch();

w1.Start();
for (int i = 0; i < 1000000; i++)
{
    PersonA personA1 = deepCopy(person);
}
w1.Stop();
Console.WriteLine(w1.ElapsedMilliseconds);


w2.Start();
for (int i = 0; i < 1000000; i++)
{
    PersonA personA2 = func(person);
}
w2.Stop();
Console.WriteLine(w2.ElapsedMilliseconds);
Console.ReadLine();

结果:差距比较大的

2185
71
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/979522.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号