如何在表达式树中实现与Where一致的Contains变量引用?
如何让表达式树的Contains调用引用外部变量而非常量数组
当构建表达式树调用Enumerable.Contains时,当前代码直接将数组作为常量传入,生成的表达式为:
.Call System.Linq.Enumerable.Contains( .Constant<System.String[]>(System.String[]), $b.FirstName)
但手写LINQ语句customers.Where(x => abc.Contains(x.FirstName))生成的表达式会引用闭包中的外部变量:
.Call System.Linq.Enumerable.Contains( .Constant<Program+<>c__DisplayClass0_0>(Program+<>c__DisplayClass0_0).abc, $x.FirstName)
需要修改表达式树代码,使其生成的调用结构与手写LINQ一致。
解决方案
核心思路是模拟编译器处理闭包的逻辑:将外部变量封装到容器类(闭包类)中,在表达式树里引用该容器实例的成员,而非直接传入变量值作为常量。
方法一:利用匿名类型模拟闭包
匿名类型可以快速模拟编译器自动生成的闭包结构:
var parameter = Expression.Parameter(typeof(Customer), "b"); var property = Expression.Property(parameter, "FirstName"); // 用匿名类型封装外部变量,模拟闭包 var closure = new { abc = new string[] { "a", "b" } }; // 获取匿名类型的abc属性 var abcProperty = closure.GetType().GetProperty("abc"); // 构建引用闭包实例及其abc属性的表达式 var closureExpr = Expression.Constant(closure); var abcExpr = Expression.Property(closureExpr, abcProperty); // 获取Enumerable.Contains方法 MethodInfo contains = typeof(Enumerable) .GetMethods(BindingFlags.Static | BindingFlags.Public) .Where(x => x.Name == nameof(Enumerable.Contains)) .Single(x => x.GetParameters().Length == 2) .MakeGenericMethod(property.Type); // 构建Contains调用表达式 var body = Expression.Call(contains, abcExpr, property);
方法二:自定义闭包类
如果需要更明确的结构,可以手动定义闭包类:
// 自定义闭包类,持有外部变量 private class QueryClosure { public string[] abc; } // 构建表达式树的代码 var parameter = Expression.Parameter(typeof(Customer), "b"); var property = Expression.Property(parameter, "FirstName"); var closure = new QueryClosure { abc = new string[] { "a", "b" } }; // 构建引用闭包实例abc字段的表达式 var abcExpr = Expression.Field(Expression.Constant(closure), nameof(QueryClosure.abc)); MethodInfo contains = typeof(Enumerable) .GetMethods(BindingFlags.Static | BindingFlags.Public) .Where(x => x.Name == nameof(Enumerable.Contains)) .Single(x => x.GetParameters().Length == 2) .MakeGenericMethod(property.Type); var body = Expression.Call(contains, abcExpr, property);
原理说明
手写LINQ时,C#编译器会自动生成一个隐藏的闭包类,将外部变量作为该类的字段/属性保存,然后在表达式中引用这个闭包实例的成员。我们的修改就是手动复现这个逻辑:
- 避免使用
Expression.Constant(abc)直接传入数组值,而是将数组放入容器对象 - 通过
Expression.Property或Expression.Field引用容器对象中的数组成员 - 最终生成的表达式会和手写LINQ一致,引用闭包实例的变量而非嵌入常量
内容的提问来源于stack exchange,提问作者Zulander
相关产品推荐
相关产品推荐

