当然可以。
简单的方法
var tests = new List<Func<int, bool>>() {
(x) => x > 10,
(x) => x < 100,
(x) => x != 42
};
我们将通过将每个谓词与已经存在的结果进行渐进式逻辑与运算,将所有这些谓词聚合为一个。由于我们需要从某个地方开始,我们将从x => true 开始,因为该谓词在执行AND 时是中性的(如果您是OR,则以x => false 开头):
var seed = (Func<int, bool>)(x => true);
var allTogether = tests.Aggregate(
seed,
(combined, expr) => (Func<int, bool>)(x => combined(x) && expr(x)));
Console.WriteLine(allTogether.Invoke(30)); // True
这很容易!但它确实有一些限制:
- 它仅适用于对象(就像您的示例一样)
- 当您的谓词列表变大(所有这些函数调用)时,它可能会有点低效
困难的方式(使用表达式树而不是编译的 lambda)
这将适用于任何地方(例如,您也可以使用它将谓词传递给 SQL 提供程序,例如 Entity Framework),并且在任何情况下它都会给出更“紧凑”的最终结果。但要让它发挥作用会困难得多。让我们开始吧。
首先,将您的输入更改为表达式树。这很简单,因为编译器会为您完成所有工作:
var tests = new List<Expression<Func<int, bool>>>() {
(x) => x > 10,
(x) => x < 100,
(x) => x != 42
};
然后将这些表达式的主体聚合为一个,与之前的想法相同。不幸的是,这不是微不足道的,它不会一直工作,但请耐心等待:
var seed = (Expression<Func<int, bool>>)
Expression.Lambda(Expression.Constant(true),
Expression.Parameter(typeof(int), "x"));
var allTogether = tests.Aggregate(
seed,
(combined, expr) => (Expression<Func<int, bool>>)
Expression.Lambda(
Expression.And(combined.Body, expr.Body),
expr.Parameters
));
现在我们在这里所做的是从所有单独的谓词构建一个巨大的BinaryExpression 表达式。
您现在可以将结果传递给 EF 或告诉编译器将其转换为代码并运行它,您将免费获得短路:
Console.WriteLine(allTogether.Compile().Invoke(30)); // should be "true"
很遗憾,由于深奥的技术原因,这最后一步不起作用。
但是为什么不行呢?
因为allTogether 表示的表达式树有点像这样:
FUNCTION
PARAMETERS: PARAM(x)
BODY: AND +-- NOT-EQUAL +---> PARAM(x)
| \---> CONSTANT(42)
|
AND +-- LESS-THAN +---> PARAM(x)
| \---> CONSTANT(100)
|
AND +-- GREATER-THAN +---> PARAM(x)
| \---> CONSTANT(10)
|
TRUE
上面树中的每个节点代表了要编译的表达式树中的一个Expression对象。问题是所有这 4 个 PARAM(x) 节点,虽然逻辑上相同,但实际上是 不同的实例(这有助于编译器通过自动创建表达式树来帮助我们?嗯,每个自然都有自己的参数实例),而要使最终结果起作用它们必须是相同的实例。我知道这是因为it has bitten me in the past。
所以,这里需要做的是迭代生成的表达式树,找到每个出现的 ParameterExpression 并用相同的实例替换它们中的每一个。同样的实例也将是构造seed时使用的第二个参数。
展示如何做到这一点将使这个答案比它有任何权利更长,但无论如何让我们这样做。我不会过多评论,你应该知道这里发生了什么:
class Visitor : ExpressionVisitor
{
private Expression param;
public Visitor(Expression param)
{
this.param = param;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return param;
}
}
然后:
var param = Expression.Parameter(typeof(int), "x");
var seed = (Expression<Func<int, bool>>)
Expression.Lambda(Expression.Constant(true),
param);
var visitor = new Visitor(param);
var allTogether = tests.Aggregate(
seed,
(combined, expr) => (Expression<Func<int, bool>>)
Expression.Lambda(
Expression.And(combined.Body, expr.Body),
param
),
lambda => (Expression<Func<int, bool>>)
// replacing all ParameterExpressions with same instance happens here
Expression.Lambda(visitor.Visit(lambda.Body), param)
);
Console.WriteLine(allTogether.Compile().Invoke(30)); // "True" -- works!