动态构建 Linq 表达式






4.80/5 (13投票s)
C# 中的引擎,
引言
嗯,总体的想法是让你能够写出这样的代码
Func<Order,bool> filterOrders =
XXX("not(Supplier.City<>\"London\" or Supplier.Status>15)");
然后将其用于任何需要 Func
或 Predicate
的表达式中。
本文将介绍如何将类似“not(Supplier.City<>\"London\" or Supplier.Status>15)
”的 string
转换为 Func<Order, bool>
。
构建这样一个引擎的想法来自于我开发一个用于商品和客户管理的桌面应用程序。其中有很多像客户、商品、订单等对象,它们以表格形式呈现给用户,用户可以进行操作。重要的一点是,用户应该能够设置自定义过滤器,根据自己的标准对项目进行排序,并添加特殊的检查条件,例如如果商品的保质期即将到期,则用不同的颜色标记它等等。
在创建这个应用程序时,我决定编写一个通用的引擎,它允许我从 string
表达式构建谓词和排序标准。当时是 .NET 2.0,还没有 Linq,所以我自己构建了一个 string
表达式解析器和解释器构建器(我使用了解释器模式)。为了访问对象的属性和函数,我使用了反射,所有这些都运行得很好……只有一个例外:由于反射,它有点慢。说有点慢是因为我花了一些时间来优化它。
现在有了 .NET 4.0 和 Linq Expressions,我决定重构它并构建 Linq Expressions。基本上,我改变的是我放弃了解释器模式(用 Linq Expression 取代它)。更重要的是:你可以编译表达式,因此性能非常好,而且没有反射。
背景
我使用 C# 泛型集合扩展方法和 Linq Expressions 来展示结果。
对于引擎本身,我使用运行时 Expressions 构建和编译(Expression.Lambda.Compile
方法)。在表达式构建过程中,我使用反射来搜索对象的属性、方法和类型转换。当然,我使用泛型。
所有表达式字符串的解析和解释器的构建都是手工完成的,无需特殊知识。
在本文中,我将 functor 理解为泛型委托:Func<T,TResult>
、Func<T1,T2,TResult>
等。下面的例子描述了一个 functor 的简单用法。
Func<int,bool> func = a => a + 1 == 7;
...
int arg0 = 6;
...
bool b=func(arg0); //b=true!!!
现在您可以看到,要使用 functor,您必须向其传递一些参数。在 C# 中,Func
最多声明支持 4 个参数。Functor 指的是传递的参数用于计算结果。
问题定义
最后,问题定义如下:
根据某个语法,从字符串表达式构造 functor Func<...,TResult>。
解决方案
我对上述问题解决方案是一个库,它允许从 string
表达式创建 Linq Expressions 和 Functors。
其中一个想法是保持表达式语法对用户友好,以便用户不必考虑变量、常量等的类型。但是,对于开发人员来说,表达式提供了指示参数、进行类型转换等可能性。尽管如此,我并不打算构建一个功能齐全的编译器,所以我省略了诸如 ?: 或 ?? 之类的运算符。不支持“if
-then
-else
”或循环。哦,对了,支持可空类型!
近似语法(类似于 C#,但不完全相同)
const
= 任何有效的常量表达式,string
可以用 "" 括起来。argument
=arg0
、arg1
、...argN
// 不区分大小写bin_operator
= +|-|*|/|=|<|<=|<>|>|>=|and|or|xor // 不区分大小写un_operator
= not // 不区分大小写type_convertor
=bool
、byte
、char
、short
、ushort
、int
、uint
、long
、ulong
、float
、double
、decimal
、string
、DateTime
// 不区分大小写type_conversion
=type_convertor
(expression
)bin_operation
=expression bin_operator expression
un_operation
=un_operator expression
property
=[arg0].PropName
|argument.PropName
|(expression).PropName
arg_list
=expression
,expression
, ... ,expression
function_call
=expression.FuncName(arg_list)
expression
=const
|argument
|property
|function_call
|bin_operation
|un_operation
生成分几个步骤完成。
ExpressionParser
类解析String
表达式并创建 Token 列表。ExpressionBuilder
类分析 Token 并创建resulting Linq Expression。- 从表达式生成 Linq Lambda 函数并编译为
Func<>
。
遇到的问题
除了构建相对简单的解析器和更复杂的编译器(构建器)之外,我还遇到了以下几个问题。
类型(部分解决)
要构建 Linq Expression,我需要为其提供特定类型的参数。例如,调用 Expression.And(Expression arg1,Expression arg2)
方法时,arg1
和 arg2
的类型应为 bool
。
对于 Expression.Add(Expression arg1,Expression arg2)
方法,arg1
和 arg2
可以是任何类型,但必须定义 + 运算符。
当从 Property
或 Function
获取的运算符不是基本类型时,我无法对其进行任何操作,只能抛出异常(我认为这是正确的处理方式)。然而,当存在常量时(尤其是在运算符两侧都是常量时,如 3 >= 4.5),我应该猜测选择哪种类型。
我不喜欢当前的解决方案,但它似乎在大多数情况下都有效:我尝试从 bool
、int
、float
等开始探测每种基本类型,以用于两个操作数。在提到的情况下,引擎决定使用 float
,因为 4.5 是 float
,而 3 是 int
,可以转换为 float
。
函数调用(部分解决)
当表达式包含函数调用时,函数的参数在 ()
中传递。引擎会检查参数数量和参数类型,如果不匹配,则抛出异常。然而,目前它没有考虑函数重载,因此有效的表达式构建可能会失败。这是因为在获取函数的 MethodInfo
之前,不会分析参数类型。这部分与上面提到的类型问题有关。
最后可以解决这个问题,但这需要更多的工作。
Using the Code
项目是 .NET 4.0 和 C#,但可以通过少量修改在 .NET 3.0 中编译(需要将某些构造函数的默认参数替换为第二个构造函数)。
在演示项目的所有示例中,我使用了几个相互关联的类:Project
、Detail
、Supplier
和 Order
。基本上,Order
实体描述了从指定的 Supplier
和 Project
订购的指定 Details
的数量。
Supplier
:属性Name
、Status
和City
Detail
:属性Name
、Weight
和City
Project
:属性Name
和City
Order
:属性Supplier
、Detail
、Project
和Quantity
Func<Order,bool> londonFilter =
ExpressionBuilder.BuildFunctor<Order,bool>(
"Project.City=\"London\" and Detail.City=Project.City");
Func<Order,string> ordStrConv =
ExpressionBuilder.BuildFunctor<Order,string>("Detail.Color.ToString()");
List<string> privelegeSuppliers =
new List<string>(orders.Where(londonFilter).Select(ordStrConv).Distinct());
Func<Supplier, Supplier, int> suppsorter =
ExpressionBuilder.BuildFunctor<Supplier, Supplier, int>("Status-arg1.Status");
suppliers.Sort(suppsorter.Invoke);
在 RequestEngineTest
项目中,您可以找到更多有趣的用法示例。
关注点
在创建这个库的过程中,我无疑对 Linq 和 Expressions 有了更深入的了解。从一开始,最终解决方案就不应该如此深度集成到内置的 Expressions 和集合扩展中。我实际上很惊讶地发现它集成得很好。
题外话
库中有一个 ListSegment<T>
类,我认为它可以重构并包含到泛型集合库中。它是 ArraySegment<T>
的类似物,但更有用。
总的来说,它透明地包装 List<T>
,以便只访问其部分元素。由于这不是我的主要目的,所以我没有完成它,还有很多工作要做。
有什么不同?
您可能会问,为什么不使用像 Jint 这样的脚本引擎?
答案是:每个工具都最适合其用途。我的目的是有一个用户友好的语法来输入自定义的排序、过滤等标准。我认为我已经实现了。
第二个答案是性能。由于 Expression 最终被编译为 IL 代码和本机代码,因此其性能远优于脚本引擎。
历史与未来
我在一年多前就成功使用过 ExpressionsGenerator
的前一个版本(基于解释器模式和反射),并且计划在将来使用这个新版本。
我计划支持更多功能,例如:
Static
方法调用- 改进的函数调用
- 或许能以某种方式添加
if
-then
-else
。 - 通过
string
表达式构建IComparer<T>
,以便可以使用自定义比较器进行排序(以前的版本就有)。 - 添加
Action<T>
支持。 - 重构:使用
AbstractFactory
模式将ExpressionsBuilder
与Expression
分离(这将允许基于相同的语法构建其他构造)。 - 重构:增加更轻松地添加新运算符的可能性。
- 审查类型处理。
哦,是的,这是一项艰巨的任务,但由于我计划使用它,所以我可能会一步一步地完成。