什么是表达式目录树Expression
表达式目录树的本质
以前我们写Linq查询条件像这样:
new List<int>().Where(i => i > 10);
我们AsQueryable
()一下:
new List<int>().AsQueryable().Where(i => i > 10);
我们查看AsQueryable
的where
定义:
...
public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);
...
从定义我们能看到AsQueryable
传入的不是一个委托,而是Expression
,这个就是表达式目录树,从定义中我们能看到表达式目录树就是Expression<T>
中T就是一个委托的类型,就构成一个表达式目录树。
Func<int, int, int> func = (m, n) => m * n + 2;
//lambda实例化委托 是个方法 是实例化委托的参数
Expression<Func<int, int, int>> exp = (m, n) => m * n + 2;
从上能看到表达式目录树和委托的实例没有什么区别,但是两者是完全是不同的东西,委托实例,右边是方法。而对于表达式目录树的右边其实是一个数据结构,描述数据与数据之间的关系,而不是一个方法语句体,所以如果我们这么写:
Expression<Func<int, int, int>> exp1 = (m, n) =>
{
return m * n + 2;
};//不能有语句体 只能是一行,不能有大括号
这样写是会报错的,不能有语句体,我们平时见到的lambda表达式的声明表达式目录树其实就是易总语法的快捷方式,lambda就像声明了多个变量以及变量之间的操作啊关系的,需要的时候还能解开。
看着表达式目录树和委托的之间的关系,是不是在想,表达式目录树可不可以想委托一样调用,是可以的:
int iResult1 = func.Invoke(12, 23);
int iResult2 = exp.Compile().Invoke(12, 23);
//exp.Compile()之后就是一个委托
我们通过安装vs的工具ExpressionTreeVisualizer来看一下表达式目录树的实际结构,安装完成后再调试,表达式目录调试显示弹窗就会多一个放大镜:
通过上面的结构解析我们能清晰的看到这个表达式结构是对一个表达式的详细描述。就是对这个表达式的详细解剖,一层一层的剖析下去直到不能解析,如下图(图片与上面的类型结构多了一个+3,原理是一样的,相当于多了一层剖析)一样剖析:
所以才说表达是目录树是一个语法树,或者说一种数据结构。那这个有什么用呢。。下面我们就需要根据这个原理来手动拼装表达式目录树,而不是通过委托来,更加深入的了解表达式目录树到底是怎么构成的。
表达式目录树的拼装
手动拼装表达式目录树,不是用的lambda的快捷方式
拼装示例一
表达式目录树快捷方式声明:Expression<Func<int>> expression = () => 123 + 234;
首先我们通过反编译软件把编译的表达式再反编译成C#看看反编译后,这个表达式是怎么写的:
Expression<Func<int>> expression = Expression.Lambda<Func<int>>(Expression.Constant(357, typeof(int)), new ParameterExpression[0]);
//Expression.Constant(357, typeof(int)),编译器已经帮我们相加了,但是我们还是要分解来讲
通过上面我就看到了这个完整的表达式目录树的声明,有时候因为语法的简化,我们看不到有些细节,但是这些细节在编译器里帮我们完成了,所以我们可以通过反编译工具反编译编译后的代码来查看有些详细的写法
。
现在我们在上面详细的声明写法来分解,再拼装起来。
通过分析expression
的详细定义,我们知道lamda定义的数据结构就是两个数字的相加,right和left的表达式都是一个简单的数字,那么首先安装表达式目录树的方式定义两个常量123和234
ConstantExpression right = Expression.Constant(234);
ConstantExpression left = Expression.Constant(123);
我们再来定义左右表达式的运算符
BinaryExpression plus = Expression.Add(left, right);
最后拼装就是:
Expression<Func<int>> expression = Expression.Lambda<Func<int>>(plus, new ParameterExpression[] { });
int iResult = expression.Compile().Invoke();
是不是和反编译的差不多,因为编译器的原因有些部分会不一样,因为编译器有些我们c#不能写的中间类型。
拼装示例二
表达式目录树快捷方式声明:Expression<Func<int, int, int>> expression = (m, n) => m * n + m + n + 2;
同样的步骤,通过反编译工具查看详细的代码写法:
ParameterExpression parameterExpression = Expression.Parameter(typeof(int), "m");
ParameterExpression parameterExpression2 = Expression.Parameter(typeof(int), "n");
Expression<Func<int, int, int>> expression = Expression.Lambda<Func<int, int, int>>(Expression.Add(Expression.Add(Expression.Add(Expression.Multiply(parameterExpression, parameterExpression2), parameterExpression), parameterExpression2), Expression.Constant(2, typeof(int))), new ParameterExpression[]
{
parameterExpression,
parameterExpression2
});
根据上面反编译的写法逐步定义拼装:
首先定义两个为int类型参数m,n:
ParameterExpression m = Expression.Parameter(typeof(int), "m");
ParameterExpression n = Expression.Parameter(typeof(int), "n");
再定义一个常量:
var constant = Expression.Constant(2);
再多左右表达式层层解析:
var mutiply = Expression.Multiply(m, n);//m*n
var plus1 = Expression.Add(mutiply, m);//m * n + m
var plus2 = Expression.Add(plus1, n);//m * n + m + n
var plus3 = Expression.Add(plus2, constant);// m * n + m + n + 2
最后的拼装:
Expression<Func<int, int, int>> expression = Expression.Lambda<Func<int, int, int>>(plus3, new ParameterExpression[] { m, n });
int iResult = expression.Compile().Invoke(23, 34);
拼装示例三
表达式目录树快捷方式声明:Expression<Func<People, bool>> lambda = x => x.Id.ToString().Equals("5");
public class People
{
public int Age { get; set; }
public string Name { get; set; }
public int Id;
}
同样的方式,反编译工具得到的结果:
parameterExpression2 = Expression.Parameter(typeof(People), "x");
Expression<Func<People, bool>> expression2 = Expression.Lambda<Func<People, bool>>(Expression.Call(Expression.Call(Expression.Field(parameterExpression2, FieldInfo.GetFieldFromHandle(ldtoken(Id))), (MethodInfo)MethodBase.GetMethodFromHandle(ldtoken(ToString())), new Expression[0]), (MethodInfo)MethodBase.GetMethodFromHandle(ldtoken(Equals())), new Expression[]
{
Expression.Constant("5", typeof(string))
}), new ParameterExpression[]
{
parameterExpression2
});
手写定义x参数:
ParameterExpression parameterExpression = Expression.Parameter(typeof(People), "x");
再定义一个常量:
var constantExp = Expression.Constant("5");
其中有一个我们c#写不出来的类ldtoken(中间语言的一种写法,C#不能直接写),我们可以通过分析得出这个类是干什么用的我用自己的方式写,分析FieldInfo.GetFieldFromHandle(ldtoken(Id))
其实就是获取Id属性,那么这么写:
FieldInfo field = typeof(People).GetField("Id");
获取到属性id后,我们要获取id的值:
var fieldExp = Expression.Field(parameterExpression, field);//就这就是x.Id
调用了一个toSting()方法,找toString()方法:
var toString = typeof(int).GetMethod("ToString", new Type[] { });//调用不带参数的toSting()
var toStringExp = Expression.Call(fieldExp, toString, new Expression[0]);
下面就是找Equals方法:
var equals = typeof(string).GetMethod("Equals", new Type[] { typeof(string) });//注意
var equalsExp = Expression.Call(toStringExp, equals, new Expression[] { constantExp });
最后替换拼装就是:
Expression<Func<People, bool>> expression = Expression.Lambda<Func<People, bool>>(equalsExp, new ParameterExpression[]
{
parameterExpression
});
bool bResult = expression.Compile()(new People()
{
Id = 5,
Name = "冰封的心",
Age = 28
});
表达式应用场景
拼装用户输入条件
我们平时有一个最常用的场景,根据用户输入的条件构造一个sql语句,一般我们是如下面那样写:
以前根据用户输入拼装条件
string sql = "SELECT * FROM USER WHERE 1=1";
Console.WriteLine("用户输入个名称,为空就跳过");
string name = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(name))
{
sql += $" and name like '%{name}%'";
}
Console.WriteLine("用户输入个账号,为空就跳过");
string account = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(account))
{
sql += $" and account like '%{account}%'";
}
我们升级一下使用Linq to SQL
var dbSet = new List<People>().AsQueryable();//EF DbSet
dbSet.Where(p => p.Id > 10 & p.Name.Contains("Eleven"));
Expression<Func<People, bool>> exp = null;
Console.WriteLine("用户输入个名称,为空就跳过");
string name = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(name))
{
exp = p => p.Name.Contains(name);
//dbSet=dbSet.Where(p => p.Name.Contains(name));
}
Console.WriteLine("用户输入个最小年纪,为空就跳过");
string age = Console.ReadLine();
if (!string.IsNullOrWhiteSpace(age) && int.TryParse(age, out int iAge))
{
exp = p => p.Age > iAge;
//dbSet = dbSet.Where(p => p => p.Age > iAge);
}
//也许都可以呢?
{
//exp= p => p.Name.Contains("Eleven") && p.Age > 5;
}
为了实现上面的场景,我们发现多来几个条件,根本没法写!,linq没法合并条件,如果直接基于dbSet来计算? 不对! 暴露dbSet,这是一整张表,很容易出事儿。
我们看看需求,发现如果能够实现根据字符串+条件自动拼装起来就好了,我们可以通过封装一个表达式目录树来自动生成,根据用户界面的输入:
ParameterExpression parameterExpression = Expression.Parameter(typeof(People), "p");
//if(name 不为空)
var name = typeof(People).GetProperty("Name");
var eleven = Expression.Constant("Eleven", typeof(string));
var nameExp = Expression.Property(parameterExpression, name);
var contains = typeof(string).GetMethod("Contains");
var containsExp = Expression.Call(nameExp, contains, new Expression[] { eleven });
//if(Age 是输入了)
var age = typeof(People).GetProperty("Age");
var age5 = Expression.Constant(5);
var ageExp = Expression.Property(parameterExpression, age);
var greatorThan = Expression.GreaterThan(ageExp, age5);
//if()
var body = Expression.AndAlso(containsExp, greatorThan);
Expression<Func<People, bool>> expression = Expression.Lambda<Func<People, bool>>(body, new ParameterExpression[] { parameterExpression });
expression.Compile()(new People()
{
Id = 10,
Name = "Eleven123"
});
上面的写法就是相当于把Linq的语法拼装出来,这样我们就可以封装一个表达式目录树自动生成。
类的转换
我们定一个这样的需求我们定义一个People
,在定义一个和people一模一样的PeopleCopy
,我们的需求就是把People
的实例转换为PeopleCopy
首先我们用的正常的方式就是:
People people = new People()
{
Id = 11,
Name = "Eleven",
Age = 31
};
PeopleCopy peopleCopy = new PeopleCopy()
{
Id = people.Id,
Name = people.Name,
Age = people.Age
};
假设程序中有很多这样的转换,为每个类型都这样硬编码太麻烦,有没有别的方法呢,一般我们还有两种办法一个是反射,一个序列化与反序列化
反射:
public class ReflectionMapper
{
/// <summary>
/// 反射
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
/// <param name="tIn"></param>
/// <returns></returns>
public static TOut Trans<TIn, TOut>(TIn tIn)
{
TOut tOut = Activator.CreateInstance<TOut>();
foreach (var itemOut in tOut.GetType().GetProperties())
{
var propIn = tIn.GetType().GetProperty(itemOut.Name);
itemOut.SetValue(tOut, propIn.GetValue(tIn));
//foreach (var itemIn in tIn.GetType().GetProperties())
//{
// if (itemOut.Name.Equals(itemIn.Name))
// {
// itemOut.SetValue(tOut, itemIn.GetValue(tIn));
// break;
// }
//}
}
foreach (var itemOut in tOut.GetType().GetFields())
{
var fieldIn = tIn.GetType().GetField(itemOut.Name);
itemOut.SetValue(tOut, fieldIn.GetValue(tIn));
//foreach (var itemIn in tIn.GetType().GetFields())
//{
// if (itemOut.Name.Equals(itemIn.Name))
// {
// itemOut.SetValue(tOut, itemIn.GetValue(tIn));
// break;
// }
//}
}
return tOut;
}
}
PeopleCopy peopleCopy1 = ReflectionMapper.Trans<People, PeopleCopy>(people);//1 反射
序列化方式:
public class SerializeMapper
{
/// <summary>
/// 序列化反序列化方式
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
public static TOut Trans<TIn, TOut>(TIn tIn)
{
return JsonConvert.DeserializeObject<TOut>(JsonConvert.SerializeObject(tIn));
}
}
PeopleCopy peopleCopy2 = SerializeMapper.Trans<People, PeopleCopy>(people);
这样是方便了,但是通过反射和序列化的方式性能太差,有没有性能好又方便又通用的办法呢,上面的表达式数目录的拼装就用上了。
想办法去动态拼装这个委托,然后缓存下委托,后面再次转换时就没有性能损耗了
Expression<Func<People, PeopleCopy>> lambda = p =>
new PeopleCopy()
{
Id = p.Id,
Name = p.Name,
Age = p.Age
};
lambda.Compile()(people);
通过上面使用委托的方式对类拷贝,那么我们可以做通用的表达式目录树的拼装来通用实现拷贝了,还是原来过程,先通过反编译工具查看这个表达式目录树的详细代码:
ParameterExpression parameterExpression = Expression.Parameter(typeof(People), "p");
Expression<Func<People, PeopleCopy>> expression = Expression.Lambda<Func<People, PeopleCopy>>(Expression.MemberInit(Expression.New(typeof(PeopleCopy)), new MemberBinding[]
{
Expression.Bind(FieldInfo.GetFieldFromHandle(ldtoken(Id)), Expression.Field(parameterExpression, FieldInfo.GetFieldFromHandle(ldtoken(Id)))),
Expression.Bind((MethodInfo)MethodBase.GetMethodFromHandle(ldtoken(set_Name())), Expression.Property(parameterExpression, (MethodInfo)MethodBase.GetMethodFromHandle(ldtoken(get_Name())))),
Expression.Bind((MethodInfo)MethodBase.GetMethodFromHandle(ldtoken(set_Age())), Expression.Property(parameterExpression, (MethodInfo)MethodBase.GetMethodFromHandle(ldtoken(get_Age()))))
}), new ParameterExpression[]
{
parameterExpression
});
通过分析上面的代码,编写一个拼装表达式目录树转换类的方法,并增加字典缓存:
public class ExpressionMapper
{
/// <summary>
/// 字典缓存--hash分布
/// </summary>
private static Dictionary<string, object> _Dic = new Dictionary<string, object>();
/// <summary>
/// 字典缓存表达式树
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
/// <param name="tIn"></param>
/// <returns></returns>
public static TOut Trans<TIn, TOut>(TIn tIn)
{
string key = string.Format("funckey_{0}_{1}", typeof(TIn).FullName, typeof(TOut).FullName);
if (!_Dic.ContainsKey(key))
{
//开始动态拼装委托
ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
List<MemberBinding> memberBindingList = new List<MemberBinding>();
foreach (var item in typeof(TOut).GetProperties())
{
MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
MemberBinding memberBinding = Expression.Bind(item, property);
memberBindingList.Add(memberBinding);
}
foreach (var item in typeof(TOut).GetFields())
{
MemberExpression property = Expression.Field(parameterExpression, typeof(TIn).GetField(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
});
//动态拼装委托完成
Func<TIn, TOut> func = lambda.Compile();//拼装是一次性的
_Dic[key] = func;
}
return ((Func<TIn, TOut>)_Dic[key]).Invoke(tIn);//委托调用执行
}
}
PeopleCopy peopleCopy4 = ExpressionMapper.Trans<People, PeopleCopy>(people);
PeopleCopy peopleCopy5 = ExpressionMapper.Trans<People, PeopleCopy>(people);
上面是通过字典缓存,是通过哈希分布的方式查找,性能再升级一下代码直接使用泛型缓存:
/// <summary>
/// 生成表达式目录树 泛型缓存
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
public class ExpressionGenericMapper<TIn, TOut>//Mapper`2
{
private static Func<TIn, TOut> _FUNC = null;
static ExpressionGenericMapper()
{
ParameterExpression parameterExpression = Expression.Parameter(typeof(TIn), "p");
List<MemberBinding> memberBindingList = new List<MemberBinding>();
foreach (var item in typeof(TOut).GetProperties())
{
MemberExpression property = Expression.Property(parameterExpression, typeof(TIn).GetProperty(item.Name));
MemberBinding memberBinding = Expression.Bind(item, property);
memberBindingList.Add(memberBinding);
}
foreach (var item in typeof(TOut).GetFields())
{
MemberExpression property = Expression.Field(parameterExpression, typeof(TIn).GetField(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
});
_FUNC = lambda.Compile();//拼装是一次性的
}
public static TOut Trans(TIn t)
{
return _FUNC(t);
}
}
PeopleCopy peopleCopy6 = ExpressionGenericMapper<People, PeopleCopy>.Trans(people);
PeopleCopy peopleCopy7 = ExpressionGenericMapper<People, PeopleCopy>.Trans(people);
如果不考虑通用性,我们可以直接委托的方式直接转换:
Expression<Func<People, PeopleCopy>> lambda = p =>
new PeopleCopy()
{
Id = p.Id,
Name = p.Name,
Age = p.Age
};
lambda.Compile()(people);
对比上面的转换方式,我们来测试一下一百次的转换耗时:
People people = new People()
{
Id = 11,
Name = "Eleven",
Age = 31
};
long common = 0;
long generic = 0;
long cache = 0;
long reflection = 0;
long serialize = 0;
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 1_000_000; i++)
{
PeopleCopy peopleCopy = new PeopleCopy()
{
Id = people.Id,
Name = people.Name,
Age = people.Age
};
}
watch.Stop();
common = watch.ElapsedMilliseconds;
}
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 1_000_000; i++)
{
PeopleCopy peopleCopy = ReflectionMapper.Trans<People, PeopleCopy>(people);
}
watch.Stop();
reflection = watch.ElapsedMilliseconds;
}
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 1_000_000; i++)
{
PeopleCopy peopleCopy = SerializeMapper.Trans<People, PeopleCopy>(people);
}
watch.Stop();
serialize = watch.ElapsedMilliseconds;
}
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 1_000_000; i++)
{
PeopleCopy peopleCopy = ExpressionMapper.Trans<People, PeopleCopy>(people);
}
watch.Stop();
cache = watch.ElapsedMilliseconds;
}
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 1_000_000; i++)
{
PeopleCopy peopleCopy = ExpressionGenericMapper<People, PeopleCopy>.Trans(people);
}
watch.Stop();
generic = watch.ElapsedMilliseconds;
}
Console.WriteLine($"common = { common} ms");
Console.WriteLine($"reflection = { reflection} ms");
Console.WriteLine($"serialize = { serialize} ms");
Console.WriteLine($"cache = { cache} ms");
Console.WriteLine($"generic = { generic} ms");
//性能比automapper
最后结果:
我们看到性能区别是很大的。我可以看到硬编码是性能最高的,因为是使用最原始的方式,没有任何性能损耗,在反射和序列化性能都很差,而是用表达式目录树,我们发现,使用字典缓存方式和泛型缓存方式还有很大的性能区别呢,因为字典需要去寻找,还是需要耗费一些时间。我们可以看到泛型缓存的表达式目录树方式和硬编码几乎是一个性能级别。
既需要动态(通用),又要保证性能(硬编码)—动态生成硬编码—表达式目录树拼装!!