好久没更新这个系列了,最近看.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 + ;
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 + ;
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()),
Expression.AddAssign(number, Expression.Constant()),
Expression.DivideAssign(number, Expression.Constant()));
Expression<Func<int>> myAction = Expression.Lambda<Func<int>>(myBlock);
Console.WriteLine(myAction.Compile()());
//

  我们声明了一个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()),
Expression.Loop(
Expression.IfThenElse(
// if 的判断逻辑
Expression.LessThanOrEqual(loopIndex, Expression.Constant()),
// 判断逻辑通过的代码
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[] { , , }, , ));
//
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) > ; InvocationExpression invocationExpression= Expression.Invoke(
largeSumTest,
Expression.Constant(),
Expression.Constant()); 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毫秒。

C#进阶之路(六):表达式进行类的赋值的更多相关文章

  1. python小白——进阶之路——day2天-———数据类型和Number类型和str字符串

    ### -python的六大标准数据类型(1)Number 数字类型(int float bool complex)(2)String 字符串类型(3)List 列表类型(4)Tuple 元组类型(5 ...

  2. Scala进阶之路-Scala特征类与unapply反向抽取

    Scala进阶之路-Scala特征类与unapply反向抽取 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.Scala特征类分析 1>.Unit 答:用于定义返回值类型, ...

  3. 【SSH进阶之路】Hibernate映射——一对一双向关联映射(六)

    上篇博文[SSH进阶之路]Hibernate映射--一对一单向关联映射(五),我们介绍了一对一的单向关联映射,单向是指仅仅能从人(Person)这端载入身份证端(IdCard),可是反过来.不能从身份 ...

  4. Java进阶之路

    Java进阶之路——从初级程序员到架构师,从小工到专家. 怎样学习才能从一名Java初级程序员成长为一名合格的架构师,或者说一名合格的架构师应该有怎样的技术知识体系,这是不仅一个刚刚踏入职场的初级程序 ...

  5. PHP进阶之路 -- 01 PHP基础语法

    PHP进阶之路 --  PHP基础语法 windows环境下php环境 php定界符  php变量  php数据类型 数据类型转换 检测数据类型 php中三种输出方式 php字符集设置 php常量 p ...

  6. PHP进阶之路 -- 02 面向对象

    PHP进阶之路-- 之 “面向对象” 基础 概念 类的介绍和定义 类的属性 类常量 类的自动加载 构造函数和析构函数 访问控制 对象继承 范围解析操作符 static静态关键字 抽象类 对象接口 Tr ...

  7. Scala进阶之路-Scala中的高级类型

    Scala进阶之路-Scala中的高级类型 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.类型(Type)与类(Class)的区别 在Java里,一直到jdk1.5之前,我们说 ...

  8. GO语言的进阶之路-面向对象编程

    GO语言的进阶之路-面向对象编程 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 当你看完这篇文章之时,我可以说你的Golang算是入门了,何为入门?就是你去看Docker 源码能看 ...

  9. GO语言的进阶之路-go的程序结构以及包简介

    GO语言的进阶之路-go的程序结构以及包简介 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.编辑,编译和运行 A,编辑 Go程序使用UTF-8编码的纯Unicode文本编写.大 ...

随机推荐

  1. 【HackerRank】Gem Stones

    Gem Stones John has discovered various rocks. Each rock is composed of various elements, and each el ...

  2. Python编程-继承和接口

    一.继承 1.什么是继承 继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,父类又可称为基类或超类,新建的类称为派生类或子类. 继承的好处: 可以使用现有类的所有功能,并在无 ...

  3. P3794 签到题IV

    题目 P3794 签到题IV 来切道水题放松一下吧 做法 或是单调不下降的,\(gcd\)是单调不上升的 \(a_i≤5×10^5\)分成权值不同的块数应该很小,所以随便乱搞就出来了 My compl ...

  4. weblogic启动错误 Unrecognized option: -jrockit

    高版本jdk启动低版本weblogic有时会报Unrecognized option: -jrockit参数错误 这纯粹是版本问题,版本更新更换参数名称的缘故 解决方法: “%WL_HOME%\com ...

  5. 手把手教你使用eclipse+qemu+gdb来单步调试ARM内核【学习笔记】

    平台信息:linux4.0 平台:qemu 作者:庄泽彬 说明:笨叔叔的Linux视频的笔记 一.编译linux源码 export CROSS_COMPILE=arm-linux-gnueabi- e ...

  6. 【P2361】yyy棋(博弈论+贪心+模拟)

    这个题看上去本来不好处理,然而善意的题面已经基本告诉你做法了,小时候玩的那个游戏就是代码的核心.动动脑子想想,如果长和宽的积是奇数,那么一定要先手,如果是偶数,那么后手就会获胜. 好了,那么怎么处理对 ...

  7. C#将字符转换成utf8编码 GB321编码转换

    public static string get_uft8(string unicodeString) { UTF8Encoding utf8 = new UTF8Encoding(); Byte[] ...

  8. iframe标签的子父页面调用函数和属性

    在使用iframe标签时,总想通过子页面调用父页面的一些方法和属性.今天终于发现了. 1在父页面写一个函数 //让子页面来调用此方法,控制导航栏 function childfunc(){ alert ...

  9. html5学习笔记(audio)

    来源于<HTML5高级程序设计> audio api <audio controls> controls告诉浏览器显示播放控件 不指定 type 浏览器自解 oggMP3 ty ...

  10. 啥是ETL、ELT

    ETL就是Extract.Transfrom.Load即抽取.转换.加载三个英文单词首字母的集合.抽取:就是从源系统抽取需要的数据,这些源系统可以是同构也可以是异构的:比如源系统可能是Excel电子表 ...