函数数组上的逻辑运算符

本文关键字:逻辑运算符 数组 函数 | 更新日期: 2023-09-27 17:50:42

我有这个想法有一段时间了,我很好奇它是否有可能以一种有价值的方式实现。我想要一个返回布尔值的lambda表达式数组,并对其结果执行逻辑运算。下面是一个无意义的有效列表示例:

var tests = new List<Func<int, bool>>() {
    (x) => x > 10,
    (x) => x < 100,
    (x) => x != 42
};

我想做的就是

bool result = tests.And();

或执行其他逻辑操作。我意识到我可以编写一个实现ienumerable的类来实现这一点,但我想知道它是否已经完成了。显然,实现必须高效地工作,以与

相同的方式短路。
if (truthyExpression || falseyExpression)

永远不会去计算falseyExpression

我在框架中看到的唯一可能有用的东西是位数组,但我不确定如何在不预先评估每个表达式的情况下使用它,从而击败了短路的有用性。

函数数组上的逻辑运算符

您可以使用内置的Enumerable.AnyEnumerable.All扩展方法。

bool andResult = tests.All(test => test(value));
bool orResult = tests.Any(test => test(value));

当然可以。

简单的方法

var tests = new List<Func<int, bool>>() {
    (x) => x > 10,
    (x) => x < 100,
    (x) => x != 42
};

我们将通过增量逻辑并将每个谓词与已经存在的结果关联来将所有这些谓词聚合为一个。因为我们需要从某个地方开始,我们将从x => true开始,因为在做AND时谓词是中性的(如果您或,从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

那很容易!但它确实有一些限制:

  • 它只适用于对象(如您的例子)
  • 当你的谓词列表变大(所有那些函数调用)时,它可能会有点低效

困难的方法(使用表达式树而不是编译的lambdas)

这将在任何地方工作(例如,你也可以使用它来传递谓词到SQL提供程序,如实体框架),它也将在任何情况下给出一个更"紧凑"的最终结果。但要让它发挥作用要困难得多。我们开始吧。

首先,将输入更改为表达式树。这是微不足道的,因为编译器为您做了所有的工作:

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)节点,虽然逻辑上相同,但实际上是不同的实例(这有助于编译器通过自动创建表达式树给我们?好吧,每一个自然都有自己的参数(实例),而为了使最终结果工作,它们必须是相同的实例。我知道这一点,因为它曾经咬过我。

因此,这里需要做的是遍历结果表达式树,找到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! 

为了将Func<int, bool>对象序列转换为bool,您需要将一个整数应用于每个值。如果你已经知道这个整数是什么,那么你可以按照Julien的描述:

bool andResult = tests.All(test => test(value));
bool orResult = tests.Any(test => test(value));

如果您不这样做,那么您要做的是从布尔值序列中创建Func<int, bool>,而不是bool:

Func<int, bool> andResult = value => tests.All(test => test(value));
Func<int, bool> orResult = value => tests.Any(test => test(value));

我们可以很容易地将其推广为一个泛型函数:

public static Func<T, bool> And<T>(this IEnumerable<Func<T, bool>> predicates)
{
    return value => predicates.All(p => p(value));
}
public static Func<T, bool> Or<T>(this IEnumerable<Func<T, bool>> predicates)
{
    return value => predicates.Any(p => p(value));
}

允许你写:

Func<int, bool> result = tests.And();

这个怎么样?

using System;
using System.Collections.Generic;
using System.Linq;
namespace SO7
{
    class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            LogicList<int> intBasedLogicalList = new LogicList<int>(new Func<int, bool>[] {x => x<3, x => x <5, x => x<8});
            Console.WriteLine(intBasedLogicalList.And(2));
            Console.WriteLine(intBasedLogicalList.And(4));
            Console.WriteLine(intBasedLogicalList.Or(7));
            Console.WriteLine(intBasedLogicalList.Or(8));
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
    }
    public class LogicList<T> : List<Func<T, bool>>
    {
        private List<Func<T,bool>> _tests;
        public LogicList(IEnumerable<Func<T, bool>> tests)
        {
            _tests = new List<Func<T, bool>>();
            foreach(var test in tests)
            {
                _tests.Add(test);
            }
        }
        public bool And(T argument){
            foreach(var test in _tests)
            {
                if (!test(argument)){
                    return false;
                }
            }
            return true;
        }
        public bool Or(T argument){
            return _tests.Any(x => x(argument));
        }
    }
}