说起Lambda表达式,大家基本都很熟悉了,而表达式树(Expression Trees),则属于80%的工作中往往都用不到的那种技术,所以即便不是什么新技术,很多人对其理解都并不透彻。此文意图从表达式树基本技术点结合实际应用,逐步来看表达式树究竟是怎么一回事,希望能帮助读者彻底学会表达式树^_^
一、初见表达式树Expression<TDelegate>
不妨回想一下,你第一次使用表达式树是在哪里呢,比如Expression<TDelegate>?
比如在linq查询中,常常用到Where方法过滤,OrderBy排序等,调用的是IQueryable<TSource>的静态扩展类Queryable的静态扩展方法。
1 // C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\System.Core.dll 2 namespace System.Linq 3 { 4 // 5 // 摘要:提供一组用于查询实现 System.Linq.IQueryable`1 的数据结构的 static方法。 6 // 7 public static class Queryable 8 { 9 //... 10 public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate); 11 public static IOrderedQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector); 12 //... 13 } 14 }
可以看到里面的参数类型Expression<Func<TSource, bool>> predicate和Expression<Func<TSource, TKey>> keySelector,咦?这不就是表达式树(Expression<TDelegate>)吗?!是的,但不止如此。
Expression<TDelegate>是表达式树的一种,并不等同于表达式树。
二、从Expression<TDelegate>到LambdaExpression
看下面的代码:
1 Func<Person, bool> func1 = x => x.Age > 18; 2 Func<Person, string> func2 = x => x.Name; 3 Expression<Func<Person, bool>> expression1 = x => x.Age > 18; 4 Expression<Func<Person, string>> expression2 = x => x.Name; 5 //错误CS0834,无法将具有语句体的lambda表达式转换为表达式树 6 Func<Person, int, int> func3 = (x, y) => { return x.Age + y; }; 7 Action<Person, int> func4 = (x, y) => { }; 8 Expression<Func<Person, int, int>> expression3 = (x, y) => { return x.Age + y; }; 9 Expression<Action<Person, int>> expression4 = (x, y) => { };
expression1和expression2可以编译通过,expression3和expression4则编译错误,提示"无法将具有语句体的Lambda表达式转换为表达式树 "。
这是因为,我们可以将Lambda表达式赋值给泛型表达式类型变量(Expression<TDelegate>),编译器会将我们的Lambda表达式编译为表达式树,但是仅限于expression lambdas(表达式Lambdas,也叫做single-line lambdas,即不具有语句体的Lambda表达式),相对的statement lambdas(语句Lambdas,也叫做multi-line lambdas),编译器则会报错。
看看反编译后的func1,func2,expression1,expression2。
ParameterExpression expression3; Func<Person, bool> func = x => x.Age > 0x12; Func<Person, string> func2 = x => x.Name; ParameterExpression[] parameters = new ParameterExpression[] { expression3 }; Expression<Func<Person, bool>> expression = Expression.Lambda<Func<Person, bool>>(Expression.GreaterThan(Expression.Property(expression3 = Expression.Parameter(typeof(Person), "x"), (MethodInfo) methodof(Person.get_Age)), Expression.Constant(0x12, typeof(int))), parameters); ParameterExpression[] expressionArray2 = new ParameterExpression[] { expression3 }; Expression<Func<Person, string>> expression2 = Expression.Lambda<Func<Person, string>>(Expression.Property(expression3 = Expression.Parameter(typeof(Person), "x"), (MethodInfo) methodof(Person.get_Name)), expressionArray2);
expression1和expression2变成了一堆复杂的语句,这才是它们作为表达式树原有的面目^_^
而Expression<TDelegate>来自何方?它继承自LambdaExpression。
来看看Expression<TDelegate>反编译后的构造函数
1 [__DynamicallyInvokable] 2 public sealed class Expression<TDelegate> : LambdaExpression 3 { 4 // 构造函数 5 internal Expression(Expression body, string name, bool tailCall, ReadOnlyCollection<ParameterExpression> parameters) : 6 base(typeof(TDelegate), name, body, tailCall, parameters) 7 { 8 } 9 //... 10 }
构造函数什么也没有做,只是将参数传递给父类构造函数,所以可以认为Expression<TDelegate>是对LambdaExpression的一层封装,而LambdaExpression又是继承自Expression,是表达式树的一种,而且是最特别的一种,为何特别,且看下文^_^
三、何为表达式树
首先来看下面的类图,可以更直观看到Expression<TDelegate>、LambdaExpression、Expression三者的关系。
黄线表示,使用表达式Lambdas赋值给Expression<TDelegate>类型变量,TDelegate会赋值给父类LambdaExpression的Type属性(联系下前面讲的构造函数的事情 ^_^ )
重点看绿线,LambdaExpression继承自Expression,而LambdaExpression的Body属性,又是Expression类型。
哇哦,眼尖的你注意到LambdaExpression还有个Compile()方法,它会编译Body,生成表示 lambda 表达式的可执行委托,然后你就可以调用委托了,所以真正的表达式树所谓的树,就是藏在Body里啦。
So,其实Expression的子类型多达几十种,例如ParameterExpression,BinaryExpression,MethodCallExpression等等,但是只有LambdaExpression(Lambda表达式树)可以执行!
如果你还有疑问,那么任意一棵其它类型的表达式树,怎样执行呢?答案自然是创建一个新的LambdaExpression了,将表达式树作为LambdaExpression的Body,然后你懂的^_^
具体可以调用这个方法 public static Expression<TDelegate> Lambda<TDelegate>(Expression body, params ParameterExpression[] parameters);
此时,我们再来解答最初的疑问,究竟何为表达式树(Expression Trees)?
表达式树即一份树形结构存储的代码,树的每个结点又都是一个表达式树,你可以编译然后运行这份树形代码。
那么可以用它做什么呢?
- 表示Lambda表达式,这个显而易见了;
- 修改可执行代码,创建动态查询;
- 定制自己的IQueryable,通过翻译表达式树数据结构为特定的查询语言,实现对特定数据源的查询,即定制orm。与我们对各种数据库的Linq查询同理;
也就是说小到根据字段字符串参数的过滤排序等,大到定制自己的orm,你都离不开表达式树。
下篇文章中,我们将就表达式树的具体用途,来做实例展示。
四、手动创建表达式树
可以通过哪些途径来创建表达式树呢?通过上文我们知道可以使用表达式Lambda由编译器来创建Lambda表达式树,例如Expression<Func<int, bool>> lambda = num => num < 5;
而另外一个更通用的方式,就是引入System.Linq.Expressions命名空间,手动构造每一个表达式树结点,来创建表达式树。
来看几个简单的例子,一看就懂。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 var ints = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 6 Func1(ints); 7 Func2(ints); 8 Func3(ints); 9 Console.Read(); 10 } 11 /// <summary> 12 /// 目标表达式树 x => x > 5 && x <= 7 13 /// </summary> 14 public static void Func1(int[] ints) 15 { 16 //创建参数x 17 var parameter = Expression.Parameter(typeof(int), "x"); 18 //创建表达式x>5 19 var con1 = Expression.Constant(5); 20 var bin1 = Expression.GreaterThan(parameter, con1); 21 //创建表达式x<=7 22 var con2 = Expression.Constant(7); 23 var bin2 = Expression.LessThanOrEqual(parameter, con2); 24 //组合两个表达式 25 var body = Expression.And(bin1, bin2); 26 //获取Lambda表达式 27 var lambda = Expression.Lambda<Func<int, bool>>(body, parameter); 28 var ints2 = ints.Where(lambda.Compile()); 29 //执行结果 30 Console.WriteLine("\r\n Func1构造的表达式树Body:\r\n {0}", body); 31 Console.WriteLine("\r\n Func1构造的表达式树:\r\n {0}", lambda); 32 Console.WriteLine("\r\n Func1的结果:\r\n {0}", string.Join(",", ints2)); 33 } 34 /// <summary> 35 /// 目标表达式树 x => x % 2 == 0 ? x : 0 36 /// </summary> 37 public static void Func2(int[] ints) 38 { 39 //创建参数 x 40 var parameter = Expression.Parameter(typeof(int), "x"); 41 //创建表达式 x % 2 42 var con1 = Expression.Constant(2); 43 var bin1 = Expression.Modulo(parameter, con1); 44 //创建表达式 (x % 2) == 0 45 var con2 = Expression.Constant(0); 46 var bin2 = Expression.Equal(bin1, con2); 47 //创建表达式 x % 2 == 0 ? x : 0 48 var body = Expression.Condition(bin2, parameter, Expression.Constant(0)); 49 //获取Lambda表达式 50 var lambda = Expression.Lambda<Func<int, int>>(body, parameter); 51 var ints2 = ints.Select(lambda.Compile()); 52 //执行结果 53 Console.WriteLine("\r\n Func2构造的表达式树Body:\r\n {0}", body); 54 Console.WriteLine("\r\n Func2构造的表达式树:\r\n {0}", lambda); 55 Console.WriteLine("\r\n Func2的结果:\r\n {0}", string.Join(",", ints2)); 56 } 57 /// <summary> 58 /// 目标表达式树 x => Console.WriteLine(x) 59 /// </summary> 60 /// <param name="ints"></param> 61 public static void Func3(int[] ints) 62 { 63 //创建参数i 64 var parameter = Expression.Parameter(typeof(int), "x"); 65 //获取Console.WriteLine MethodInfo 66 MethodInfo method = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }); 67 //创建表达式 68 var body = Expression.Call(method, parameter); 69 var lambda = Expression.Lambda<Action<int>>(body, parameter); 70 Console.WriteLine("\r\n Func3构造的表达式树Body:\r\n {0}", body); 71 Console.WriteLine("\r\n Func3构造的表达式树:\r\n {0}", lambda); 72 Array.ForEach(ints, lambda.Compile()); 73 } 74 }
输出结果:
怎么样,动手来写几个表达式树吧?!代码实战会让你更加轻松的入门表达式树技术。
这里贴出Func1中的lambda变量监视图,看看这棵树吧^_^
通过上面几个小例子,相信你对表达式树的手动创建已经基本了解,同时可以看到手动创建远没有使用表达式Lambda来的方便,但是却可以突破single-line lambdas限制实现更多强大的功能。
那么Expression有多少种呢?看下图。这些都是继承自Expression,至于每种的用途,可以在VS中自行浏览,使用时根据我们的实际用途来选用不同的Expression来构建表达式树即可。
这些表达式目录树节点的类型共有85种。
1 // 2 // 摘要: 3 // 描述表达式目录树的节点的节点类型。 4 public enum ExpressionType 5 { 6 // 7 // 摘要: 8 // 加法运算,如 a + b,针对数值操作数,不进行溢出检查。 9 Add = 0, 10 // 11 // 摘要: 12 // 加法运算,如 (a + b),针对数值操作数,进行溢出检查。 13 AddChecked = 1, 14 // 15 // 摘要: 16 // 按位或逻辑 AND 运算,如 C# 中的 (a & b) 和 Visual Basic 中的 (a And b)。 17 And = 2, 18 // 19 // 摘要: 20 // 条件 AND 运算,它仅在第一个操作数的计算结果为 true 时才计算第二个操作数。它与 C# 中的 (a && b) 和 Visual Basic 中的 21 // (a AndAlso b) 对应。 22 AndAlso = 3, 23 // 24 // 摘要: 25 // 获取一维数组长度的运算,如 array.Length。 26 ArrayLength = 4, 27 // 28 // 摘要: 29 // 一维数组中的索引运算,如 C# 中的 array[index] 或 Visual Basic 中的 array(index)。 30 ArrayIndex = 5, 31 // 32 // 摘要: 33 // 方法调用,如在 obj.sampleMethod() 表达式中。 34 Call = 6, 35 // 36 // 摘要: 37 // 表示 null 合并运算的节点,如 C# 中的 (a ?? b) 或 Visual Basic 中的 If(a, b)。 38 Coalesce = 7, 39 // 40 // 摘要: 41 // 条件运算,如 C# 中的 a > b ? a : b 或 Visual Basic 中的 If(a > b, a, b)。 42 Conditional = 8, 43 // 44 // 摘要: 45 // 一个常量值。 46 Constant = 9, 47 // 48 // 摘要: 49 // 强制转换或转换运算,如 C#中的 (SampleType)obj 或 Visual Basic 中的 CType(obj, SampleType)。对于数值转换,如果转换后的值对于目标类型来说太大,这不会引发异常。 50 Convert = 10, 51 // 52 // 摘要: 53 // 强制转换或转换运算,如 C#中的 (SampleType)obj 或 Visual Basic 中的 CType(obj, SampleType)。对于数值转换,如果转换后的值与目标类型大小不符,则引发异常。 54 ConvertChecked = 11, 55 // 56 // 摘要: 57 // 除法运算,如 (a / b),针对数值操作数。 58 Divide = 12, 59 // 60 // 摘要: 61 // 表示相等比较的节点,如 C# 中的 (a == b) 或 Visual Basic 中的 (a = b)。 62 Equal = 13, 63 // 64 // 摘要: 65 // 按位或逻辑 XOR 运算,如 C# 中的 (a ^ b) 或 Visual Basic 中的 (a Xor b)。 66 ExclusiveOr = 14, 67 // 68 // 摘要: 69 // “大于”比较,如 (a > b)。 70 GreaterThan = 15, 71 // 72 // 摘要: 73 // “大于或等于”比较,如 (a >= b)。 74 GreaterThanOrEqual = 16, 75 // 76 // 摘要: 77 // 调用委托或 lambda 表达式的运算,如 sampleDelegate.Invoke()。 78 Invoke = 17, 79 // 80 // 摘要: 81 // lambda 表达式,如 C# 中的 a => a + a 或 Visual Basic 中的 Function(a) a + a。 82 Lambda = 18, 83 // 84 // 摘要: 85 // 按位左移运算,如 (a << b)。 86 LeftShift = 19, 87 // 88 // 摘要: 89 // “小于”比较,如 (a < b)。 90 LessThan = 20, 91 // 92 // 摘要: 93 // “小于或等于”比较,如 (a <= b)。 94 LessThanOrEqual = 21, 95 // 96 // 摘要: 97 // 创建新的 System.Collections.IEnumerable 对象并从元素列表中初始化该对象的运算,如 C# 中的 new List<SampleType>(){ 98 // a, b, c } 或 Visual Basic 中的 Dim sampleList = { a, b, c }。 99 ListInit = 22, 100 // 101 // 摘要: 102 // 从字段或属性进行读取的运算,如 obj.SampleProperty。 103 MemberAccess = 23, 104 // 105 // 摘要: 106 // 创建新的对象并初始化其一个或多个成员的运算,如 C# 中的 new Point { X = 1, Y = 2 } 或 Visual Basic 中的 New 107 // Point With {.X = 1, .Y = 2}。 108 MemberInit = 24, 109 // 110 // 摘要: 111 // 算术余数运算,如 C# 中的 (a % b) 或 Visual Basic 中的 (a Mod b)。 112 Modulo = 25, 113 // 114 // 摘要: 115 // 乘法运算,如 (a * b),针对数值操作数,不进行溢出检查。 116 Multiply = 26, 117 // 118 // 摘要: 119 // 乘法运算,如 (a * b),针对数值操作数,进行溢出检查。 120 MultiplyChecked = 27, 121 // 122 // 摘要: 123 // 算术求反运算,如 (-a)。不应就地修改 a 对象。 124 Negate = 28, 125 // 126 // 摘要: 127 // 一元加法运算,如 (+a)。预定义的一元加法运算的结果是操作数的值,但用户定义的实现可以产生特殊结果。 128 UnaryPlus = 29, 129 // 130 // 摘要: 131 // 算术求反运算,如 (-a),进行溢出检查。不应就地修改 a 对象。 132 NegateChecked = 30, 133 // 134 // 摘要: 135 // 调用构造函数创建新对象的运算,如 new SampleType()。 136 New = 31, 137 // 138 // 摘要: 139 // 创建新的一维数组并从元素列表中初始化该数组的运算,如 C# 中的 new SampleType[]{a, b, c} 或 Visual Basic 中的 140 // New SampleType(){a, b, c}。 141 NewArrayInit = 32, 142 // 143 // 摘要: 144 // 创建新数组(其中每个维度的界限均已指定)的运算,如 C# 中的 new SampleType[dim1, dim2] 或 Visual Basic 中的 145 // New SampleType(dim1, dim2)。 146 NewArrayBounds = 33, 147 // 148 // 摘要: 149 // 按位求补运算或逻辑求反运算。在 C# 中,它与整型的 (~a) 和布尔值的 (!a) 等效。在 Visual Basic 中,它与 (Not a) 等效。不应就地修改 150 // a 对象。 151 Not = 34, 152 // 153 // 摘要: 154 // 不相等比较,如 C# 中的 (a != b) 或 Visual Basic 中的 (a <> b)。 155 NotEqual = 35, 156 // 157 // 摘要: 158 // 按位或逻辑 OR 运算,如 C# 中的 (a | b) 或 Visual Basic 中的 (a Or b)。 159 Or = 36, 160 // 161 // 摘要: 162 // 短路条件 OR 运算,如 C# 中的 (a || b) 或 Visual Basic 中的 (a OrElse b)。 163 OrElse = 37, 164 // 165 // 摘要: 166 // 对在表达式上下文中定义的参数或变量的引用。有关详细信息,请参阅System.Linq.Expressions.ParameterExpression。 167 Parameter = 38, 168 // 169 // 摘要: 170 // 对某个数字进行幂运算的数学运算,如 Visual Basic 中的 (a ^ b)。 171 Power = 39, 172 // 173 // 摘要: 174 // 具有类型为 System.Linq.Expressions.Expression 的常量值的表达式。System.Linq.Expressions.ExpressionType.Quote 175 // 节点可包含对参数的引用,这些参数在该节点表示的表达式的上下文中定义。 176 Quote = 40, 177 // 178 // 摘要: 179 // 按位右移运算,如 (a >> b)。 180 RightShift = 41, 181 // 182 // 摘要: 183 // 减法运算,如 (a - b),针对数值操作数,不进行溢出检查。 184 Subtract = 42, 185 // 186 // 摘要: 187 // 算术减法运算,如 (a - b),针对数值操作数,进行溢出检查。 188 SubtractChecked = 43, 189 // 190 // 摘要: 191 // 显式引用或装箱转换,其中如果转换失败则提供 null,如 C# 中的 (obj as SampleType) 或 Visual Basic 中的 TryCast(obj, 192 // SampleType)。 193 TypeAs = 44, 194 // 195 // 摘要: 196 // 类型测试,如 C# 中的 obj is SampleType 或 Visual Basic 中的 TypeOf obj is SampleType。 197 TypeIs = 45, 198 // 199 // 摘要: 200 // 赋值运算,如 (a = b)。 201 Assign = 46, 202 // 203 // 摘要: 204 // 表达式块。 205 Block = 47, 206 // 207 // 摘要: 208 // 调试信息。 209 DebugInfo = 48, 210 // 211 // 摘要: 212 // 一元递减运算,如 C# 和 Visual Basic 中的 (a - 1)。不应就地修改 a 对象。 213 Decrement = 49, 214 // 215 // 摘要: 216 // 动态操作。 217 Dynamic = 50, 218 // 219 // 摘要: 220 // 默认值。 221 Default = 51, 222 // 223 // 摘要: 224 // 扩展表达式。 225 Extension = 52, 226 // 227 // 摘要: 228 // “跳转”表达式,如 C# 中的 goto Label 或 Visual Basic 中的 GoTo Label。 229 Goto = 53, 230 // 231 // 摘要: 232 // 一元递增运算,如 C# 和 Visual Basic 中的 (a + 1)。不应就地修改 a 对象。 233 Increment = 54, 234 // 235 // 摘要: 236 // 索引运算或访问使用参数的属性的运算。 237 Index = 55, 238 // 239 // 摘要: 240 // 标签。 241 Label = 56, 242 // 243 // 摘要: 244 // 运行时变量的列表。有关详细信息,请参阅System.Linq.Expressions.RuntimeVariablesExpression。 245 RuntimeVariables = 57, 246 // 247 // 摘要: 248 // 循环,如 for 或 while。 249 Loop = 58, 250 // 251 // 摘要: 252 // 多分支选择运算,如 C# 中的 switch 或 Visual Basic 中的 Select Case。 253 Switch = 59, 254 // 255 // 摘要: 256 // 引发异常的运算,如 throw new Exception()。 257 Throw = 60, 258 // 259 // 摘要: 260 // try-catch 表达式。 261 Try = 61, 262 // 263 // 摘要: 264 // 取消装箱值类型运算,如 MSIL 中的 unbox 和 unbox.any 指令。 265 Unbox = 62, 266 // 267 // 摘要: 268 // 加法复合赋值运算,如 (a += b),针对数值操作数,不进行溢出检查。 269 AddAssign = 63, 270 // 271 // 摘要: 272 // 按位或逻辑 AND 复合赋值运算,如 C# 中的 (a &= b)。 273 AndAssign = 64, 274 // 275 // 摘要: 276 // 除法复合赋值运算,如 (a /= b),针对数值操作数。 277 DivideAssign = 65, 278 // 279 // 摘要: 280 // 按位或逻辑 XOR 复合赋值运算,如 C# 中的 (a ^= b)。 281 ExclusiveOrAssign = 66, 282 // 283 // 摘要: 284 // 按位左移复合赋值运算,如 (a <<= b)。 285 LeftShiftAssign = 67, 286 // 287 // 摘要: 288 // 算术余数复合赋值运算,如 C# 中的 (a %= b)。 289 ModuloAssign = 68, 290 // 291 // 摘要: 292 // 乘法复合赋值运算,如 (a *= b),针对数值操作数,不进行溢出检查。 293 MultiplyAssign = 69, 294 // 295 // 摘要: 296 // 按位或逻辑 OR 复合赋值运算,如 C# 中的 (a |= b)。 297 OrAssign = 70, 298 // 299 // 摘要: 300 // 对某个数字进行幂运算的复合赋值运算,如 Visual Basic 中的 (a ^= b)。 301 PowerAssign = 71, 302 // 303 // 摘要: 304 // 按位右移复合赋值运算,如 (a >>= b)。 305 RightShiftAssign = 72, 306 // 307 // 摘要: 308 // 减法复合赋值运算,如 (a -= b),针对数值操作数,不进行溢出检查。 309 SubtractAssign = 73, 310 // 311 // 摘要: 312 // 加法复合赋值运算,如 (a += b),针对数值操作数,进行溢出检查。 313 AddAssignChecked = 74, 314 // 315 // 摘要: 316 // 乘法复合赋值运算,如 (a *= b),针对数值操作数,进行溢出检查。 317 MultiplyAssignChecked = 75, 318 // 319 // 摘要: 320 // 减法复合赋值运算,如 (a -= b),针对数值操作数,进行溢出检查。 321 SubtractAssignChecked = 76, 322 // 323 // 摘要: 324 // 一元前缀递增,如 (++a)。应就地修改 a 对象。 325 PreIncrementAssign = 77, 326 // 327 // 摘要: 328 // 一元前缀递减,如 (--a)。应就地修改 a 对象。 329 PreDecrementAssign = 78, 330 // 331 // 摘要: 332 // 一元后缀递增,如 (a++)。应就地修改 a 对象。 333 PostIncrementAssign = 79, 334 // 335 // 摘要: 336 // 一元后缀递减,如 (a--)。应就地修改 a 对象。 337 PostDecrementAssign = 80, 338 // 339 // 摘要: 340 // 确切类型测试。 341 TypeEqual = 81, 342 // 343 // 摘要: 344 // 二进制反码运算,如 C# 中的 (~a)。 345 OnesComplement = 82, 346 // 347 // 摘要: 348 // true 条件值。 349 IsTrue = 83, 350 // 351 // 摘要: 352 // false 条件值。 353 IsFalse = 84 354 }
--参考阅读--
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/