• C#进阶之路(六):表达式进行类的赋值


      好久没更新这个系列了,最近看.NET CORE源码的时候,发现他的依赖注入模块的很多地方用了表达式拼接实现的。比如如下代码

    private Expression<Func<ServiceProviderEngineScope, object>> BuildExpression(IServiceCallSite callSite)
    {
        var context = new CallSiteExpressionBuilderContext
        {
            ScopeParameter = ScopeParameter
        };
        var serviceExpression = VisitCallSite(callSite, context);
        if (context.RequiresResolvedServices)
        {
            return Expression.Lambda<Func<ServiceProviderEngineScope, object>>(
                Expression.Block(
                    new [] { ResolvedServices },
                    ResolvedServicesVariableAssignment,
                    Lock(serviceExpression, ResolvedServices)),
                ScopeParameter);
        }
        return Expression.Lambda<Func<ServiceProviderEngineScope, object>>(serviceExpression, ScopeParameter);
    }

           所以今天我们先一起了解下表达式树以及它的一种实用应用——表达式树进行类的快速赋值。

    提示:学习这一章,需要有一定拉姆达基础,如果不太了解拉姆达,推荐阅读《C#进阶之路(四):拉姆达》

    一、初识表达式树

           表达式树是将我们原来可以直接由代码编写的逻辑以表达式的方式存储在树状的结构里,从而可以在运行时去解析这个树,然后执行,实现动态的编辑和执行代码。LINQ to SQL就是通过把表达式树翻译成SQL来实现的,所以了解表达树有助于我们更好的理解 LINQ to SQL,同时如果你有兴趣,可以用它创造出很多有意思的东西来。

      根据Lambda表达式来创建表达式树,这应该是最直接的创建表达式树的方式了。

    Expression<Func<int, int>> expr = x => x + 1;
    Console.WriteLine(expr.ToString());  // x=> (x + 1)
    // 下面的代码编译不通过
    Expression<Func<int, int, int>> expr2 = (x, y) => { return x + y; };
    Expression<Action<int>> expr3 = x => {  };

      这种方式只能创建最简单的表达式树,复杂点的编译器就不认识了。

      右边是一个Lambda表达式,而左边是一个表达式树。为什么可以直接赋值呢?这个就要多亏我们的Expression<TDelegate>泛型类了。而Expression<TDelegate>是直接继承自LambdaExpression的,我们来看一下Expression的构造函数:

    internal Expression(Expression body, string name, bool tailCall,ReadOnlyCollection<ParameterExpression> parameters)
        : base(typeof(TDelegate), name, body, tailCall, parameters)
    {
    }

    实际上这个构造函数什么也没有做,只是把相关的参数传给了父类,也就是LambdaExpression,由它把我们表达式的主体,名称,以及参数保存着。

    Expression<Func<int, int>> expr = x => x + 1;
    Console.WriteLine(expr.ToString());  // x=> (x + 1)
    var lambdaExpr = expr as LambdaExpression;
    Console.WriteLine(lambdaExpr.Body);   // (x + 1)
    Console.WriteLine(lambdaExpr.ReturnType.ToString());  // System.Int32
    foreach (var parameter in lambdaExpr.Parameters)
    {
        Console.WriteLine("Name:{0}, Type:{1}, ",parameter.Name,parameter.Type.ToString());
    }
    //Name:x, Type:System.Int32

    二、创建一个复杂的Lambda表达式树

      上面我们讲到直接由Lambda表达式的方式来创建表达式树,可惜只限于一种类型。下面我们就来演示一下如何创建一个无参无返回值的表达式树。

    // 下面的方法编译不能过
    /*
    Expression<Action> lambdaExpression2 = () =>
    {
        for (int i = 1; i <= 10; i++)
        {
            Console.WriteLine("Hello");
        }
    };
    */    
    // 创建 loop表达式体来包含我们想要执行的代码
    LoopExpression loop = Expression.Loop(
        Expression.Call(
            null,
            typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
            Expression.Constant("Hello"))
            );
    // 创建一个代码块表达式包含我们上面创建的loop表达式
    BlockExpression block = Expression.Block(loop);
    
    // 将我们上面的代码块表达式
    Expression<Action> lambdaExpression =  Expression.Lambda<Action>(block);
    lambdaExpression.Compile().Invoke();

    上面我们通过手动编码的方式创建了一个无参的Action,执行了一组循环。代码很简单,重要的是我们要熟悉这些各种类型的表达式以及他们的使用方式。上面我们引入了以下类型的表达式:

     

      看起来神密的表达式树也不过如此嘛?如果大家去执行上面的代码,就会陷入死循环,我没有为loop加入break的条件。为了方便大家理解,我是真的一步一步来啊,现在我们就来终止这个循环。就像上面那一段不能编译通过的代码实现的功能一样,我们要输出10个”Hello”。

    上面我们先写了一个LoopExpression,然后把它传给了BlockExpresson,从而形成的的一块代码或者我们也可以说一个方法体。但是如果我们有多个执行块,而且这多个执行块里面需要处理同一个参数,我们就得在block里面声明这些参数了。

    ParameterExpression number=Expression.Parameter(typeof(int),"number");
    BlockExpression myBlock = Expression.Block(
        new[] { number },
        Expression.Assign(number, Expression.Constant(2)),
        Expression.AddAssign(number, Expression.Constant(6)),
        Expression.DivideAssign(number, Expression.Constant(2)));
    Expression<Func<int>> myAction = Expression.Lambda<Func<int>>(myBlock);
    Console.WriteLine(myAction.Compile()());
    // 4

      我们声明了一个int的变量并赋值为2,然后加上6最后除以2。如果我们要用变量,就必须在block的你外面声明它,并且在block里面把它引入进来。否则在该表达式树时会出现,变量不在作用域里的错。

      下面我们继续我们未完成的工作,为循环加入退出条件。为了让大家快速的理解loop的退出机制,我们先来看一段伪代码:

    LabelTarget labelBreak = Expression.Label();
    Expression.Loop(
        "如果 条件 成功"
            "执行成功的代码"
        "否则"
            Expression.Break(labelBreak) //跳出循环
        , labelBreak);

    我们需要借助于LabelTarget 以及Expression.Break来达到退出循环的目地。下面我们来看一下真实的代码:

    LabelTarget labelBreak = Expression.Label();
    ParameterExpression loopIndex = Expression.Parameter(typeof(int), "index");
     
    BlockExpression block = Expression.Block(
    new[] { loopIndex },
    // 初始化loopIndex =1
        Expression.Assign(loopIndex, Expression.Constant(1)),
        Expression.Loop(
            Expression.IfThenElse(
                // if 的判断逻辑
                Expression.LessThanOrEqual(loopIndex, Expression.Constant(10)),
                // 判断逻辑通过的代码
                Expression.Block(
                    Expression.Call(
                        null,
                        typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }),
                        Expression.Constant("Hello")),
                    Expression.PostIncrementAssign(loopIndex)),
                // 判断不通过的代码
                Expression.Break(labelBreak)
                ),labelBreak));
     
    // 将我们上面的代码块表达式
    Expression<Action> lambdaExpression =  Expression.Lambda<Action>(block);
    lambdaExpression.Compile().Invoke();

    好吧,我们又学了几个新的类型的表达式,来总结一下:

     

    到这里,我想大家应该对表达式树的构建有了一个清楚的认识。至于为什么不允许我们直接基于复杂的Lambda表达式来创建表达式树呢?

    这里的Lambda表达式实际上是一个Expression Body。

    这个Expression Body实际上就是我们上面讲到的Expression中的一种。

    也就是说编译器需要时间去分析你到底是哪一种?

    最简单的x=> x+1之类的也就是Func<TValue,TKey> 是很容易分析的。

    实际这里面允许的Expression Body只有BinaryExpression。

    最后,我们来完整的看一下.NET都为我们提供了哪些类型的表达式(下面这些类都是继承自Expression)。

     

    TypeBinaryExpression
    TypeBinaryExpression typeBinaryExpression =
        Expression.TypeIs(
            Expression.Constant("spruce"),
            typeof(int));
     
    Console.WriteLine(typeBinaryExpression.ToString());
    // ("spruce" Is Int32)
    IndexExpression
    ParameterExpression arrayExpr = Expression.Parameter(typeof(int[]), "Array");
     
    ParameterExpression indexExpr = Expression.Parameter(typeof(int), "Index");
     
    ParameterExpression valueExpr = Expression.Parameter(typeof(int), "Value");
     
    Expression arrayAccessExpr = Expression.ArrayAccess(
        arrayExpr,
        indexExpr
    );
    Expression<Func<int[], int, int, int>> lambdaExpr = Expression.Lambda<Func<int[], int, int, int>>(
            Expression.Assign(arrayAccessExpr, Expression.Add(arrayAccessExpr, valueExpr)),
            arrayExpr,
            indexExpr,
            valueExpr
        );
    Console.WriteLine(arrayAccessExpr.ToString());
    // Array[Index]
    Console.WriteLine(lambdaExpr.ToString());
    // (Array, Index, Value) => (Array[Index] = (Array[Index] + Value))
    Console.WriteLine(lambdaExpr.Compile().Invoke(new int[] { 10, 20, 30 }, 0, 5));
    // 15
    NewExpression
    NewExpression newDictionaryExpression =Expression.New(typeof(Dictionary<int, string>));
    Console.WriteLine(newDictionaryExpression.ToString());
    // new Dictionary`2()
    InvocationExpression
    Expression<Func<int, int, bool>> largeSumTest =
        (num1, num2) => (num1 + num2) > 1000;
     
    InvocationExpression invocationExpression= Expression.Invoke(
        largeSumTest,
        Expression.Constant(539),
        Expression.Constant(281));
     
    Console.WriteLine(invocationExpression.ToString());
    // Invoke((num1, num2) => ((num1 + num2) > 1000),539,281)
    表达式类型

    三、类的赋值

    在代码中经常会遇到需要把对象复制一遍,或者把属性名相同的值复制一遍。

    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; } 
        public int Age { get; set; } 
    }
    
    public class StudentSecond
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; } 
    }

      反射

    反射应该是很多人用过的方法,就是封装一个类,反射获取属性和设置属性的值。

    private static TOut TransReflection<TIn, TOut>(TIn tIn)
    {
        TOut tOut = Activator.CreateInstance<TOut>();
        var tInType = tIn.GetType();
        foreach (var itemOut in tOut.GetType().GetProperties())
        {
            var itemIn = tInType.GetProperty(itemOut.Name); ;
            if (itemIn != null)
            {
                itemOut.SetValue(tOut, itemIn.GetValue(tIn));
            }
        }
        return tOut;
    }

    调用:StudentSecond ss= TransReflection<Student, StudentSecond>(s);

    调用一百万次耗时:2464毫秒

      序列化

    序列化的方式有很多种,有二进制、xml、json等等,今天我们就用Newtonsoft的json进行测试。

    调用:StudentSecond ss= JsonConvert.DeserializeObject<StudentSecond>(JsonConvert.SerializeObject(s));

    调用一百万次耗时:2984毫秒

    从这可以看出序列化和反射效率差别不大。

    四、表达式树进行类的快速赋值

    1、简单实现

    Expression<Func<Student, StudentSecond>> ss = (x) => new StudentSecond { Age = x.Age, Id = x.Id, Name = x.Name };
    var f = ss.Compile();
    StudentSecond studentSecond = f(s);

    这样的方式我们可以达到同样的效果。

    有人说这样的写法和最原始的复制没有什么区别,代码反而变多了呢,这个只是第一步。

    2、分析代码

    我们用ILSpy反编译下这段表达式代码如下:

    ParameterExpression parameterExpression;
    Expression<Func<Student, StudentSecond>> ss = Expression.Lambda<Func<Student, StudentSecond>>(Expression.MemberInit(Expression.New(typeof(StudentSecond)), new MemberBinding[]
    {
        Expression.Bind(methodof(StudentSecond.set_Age(int)), Expression.Property(parameterExpression, methodof(Student.get_Age()))),
        Expression.Bind(methodof(StudentSecond.set_Id(int)), Expression.Property(parameterExpression, methodof(Student.get_Id()))),
        Expression.Bind(methodof(StudentSecond.set_Name(string)), Expression.Property(parameterExpression, methodof(Student.get_Name())))
    }), new ParameterExpression[]
    {
        parameterExpression
    });
    Func<Student, StudentSecond> f = ss.Compile();
    StudentSecond studentSecond = f(s);

    那么也就是说我们只要用反射循环所有的属性然后Expression.Bind所有的属性。最后调用Compile()(s)就可以获取正确的StudentSecond。

    看到这有的人又要问了,如果用反射的话那岂不是效率很低,和直接用反射或者用序列化没什么区别吗?

    当然这个可以解决的,就是我们的表达式树可以缓存。只是第一次用的时候需要反射,以后再用就不需要反射了。

    3、利用泛型的特性实现通用代码

    /// <summary>
    /// 表达式树进行对象转换  qxb
    /// </summary>
    /// <typeparam name="TIn"></typeparam>
    /// <typeparam name="TOut"></typeparam>
    public static class ExpTransHelper<TIn, TOut>
    {
        private static readonly Func<TIn, TOut> cache = GetFunc();
        private static Func<TIn, TOut> GetFunc()
        {
            ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
            List<MemberBinding> memberBindingList = new List<MemberBinding>();
            foreach (var item in typeof(TOut).GetProperties())
            {
                if (!item.CanWrite)
                    continue;
                if (typeof(TIn).GetProperty(item.Name) == null)
                {
                    if (item.PropertyType == typeof(string))
                    {
                        //将这个属性绑定到""
                        MemberBinding memberString = Expression.Bind(item, Expression.Constant(""));
                        memberBindingList.Add(memberString);
                    }
                    continue;
                }
                MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
                MemberBinding memberBinding = Expression.Bind(item, property);
                memberBindingList.Add(memberBinding);
            }
            MemberInitExpression memberInitExpression = Expression.MemberInit(Expression.New(typeof(TOut)), memberBindingList.ToArray());
            Expression<Func<TIn, TOut>> lambda = Expression.Lambda<Func<TIn, TOut>>(memberInitExpression, new ParameterExpression[] { parameterExpression });
    
            return lambda.Compile();
        }
    
        public static TOut Trans(TIn tIn)
        {
            return cache(tIn);
        }
    }

    调用:StudentSecond ss= TransExpV2<Student, StudentSecond>.Trans(s);

    调用一百万次耗时:107毫秒

    耗时小于使用automapper的338毫秒。

  • 相关阅读:
    windows server 2019安装
    python 求相关系数
    MySQL的Sleep进程占用大量连接解决方法
    mysql show processlist分析
    mysql5.6常用查询sql
    使用exe4j将java项目打成exe执行程序
    MediaWIKI部署流程
    谈谈Activiti中流程对象之间的关系
    EhCache RMI 分布式缓存/缓存集群
    Tomcat8安装, 安全配置与性能优化
  • 原文地址:https://www.cnblogs.com/qixinbo/p/9604234.html
Copyright © 2020-2023  润新知