简体   繁体   English

函数数组上的逻辑运算符

[英]Logical operator on array of functions

I've had this idea for a while, and I'm curious if it's possible to implement in a worthwhile way. 我已经有一段时间这个想法了,我很好奇是否有可能以一种有价值的方式来实现。 I want to take an array of boolean-returning lambda expressions and perform logical operations on their results. 我想获取一组返回布尔值的lambda表达式,并对它们的结果执行逻辑运算。 Here's a pointless example of a valid list: 这是有效列表的毫无意义的示例:

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

What I'd like to do is essentially 我想做的基本上是

bool result = tests.And();

or perform other logical operations. 或执行其他逻辑运算。 I realize I could write an IEnumerable-implementing class and do this, but I was wondering if it has already been done. 我意识到我可以编写IEnumerable-implementing类并执行此操作,但是我想知道它是否已经完成。 Obviously, the implementation has to work efficiently, short-circuiting in the same way as 显然,实施必须有效地工作,并且短路方式与

if (truthyExpression || falseyExpression)

would never get around to evaluating falseyExpression . 永远不会绕过评估falseyExpression

The only thing I see in the framework that could maybe be helpful is Bit Array , but I'm not sure how I could use that without pre-evaluating every expression, defeating the usefulness of short-cicuiting. 我在框架中看到的唯一可能有用的东西是Bit Array ,但我不确定如何在不预先评估每个表达式的情况下使用它,从而打败了简短提示的用处。

You could use the built-in Enumerable.Any and Enumerable.All extension methods. 您可以使用内置的Enumerable.AnyEnumerable.All扩展方法。

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

Sure you could. 当然可以。

The easy way 简单的方法

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

We 're going to aggregate all these predicates into one by incrementally logical-and-ing each one with the already existing result. 我们将通过递增逻辑将所有这些谓词聚合为一个,并将每个谓词与已经存在的结果相加。 Since we need to start somewhere, we 'll start with x => true because that predicate is neutral when doing AND (start with x => false if you OR): 由于我们需要从某处开始,所以我们将从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

That was easy! 那很简单! It does have a few limitations though: 但是它确实有一些限制:

  • it only works on objects (as does your example) 它仅适用于对象(如您的示例)
  • it might be a tiny bit inefficient when your list of predicates gets large (all those function calls) 当您的谓词列表变大(所有这些函数调用)时,效率可能会略有降低

The hard way (using expression trees instead of compiled lambdas) 困难的方法(使用表达式树而不是编译的lambda)

This will work everywhere (eg you can also use it to pass predicates to SQL providers such as Entity Framework) and it will also give a more "compact" final result in any case. 这将在任何地方都有效(例如,您也可以使用它将谓词传递给诸如实体框架之类的SQL提供程序),并且在任何情况下都将提供更“紧凑”的最终结果。 But it's going to be much harder to make it work. 但是要使其正常工作将更加困难。 Let's get to it. 让我们开始吧。

First, change your input to be expression trees. 首先,将输入更改为表达式树。 This is trivial because the compiler does all the work for you: 这很简单,因为编译器会为您完成所有工作:

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

Then aggregate the bodies of these expressions into one, the same idea as before. 然后将这些表达式的主体聚合为一个,和以前一样。 Unfortunately, this is not trivial and it is not going to work all the way, but bear with me: 不幸的是,这并非微不足道,并且不会一直有效,但请忍受:

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
    ));

Now what we did here was build one giant BinaryExpression expression from all the individual predicates. 现在,我们在这里所做的是从所有单个谓词构建一个巨大的BinaryExpression表达式。

You can now pass the result to EF or tell the compiler to turn this into code for you and run it, and you get short-circuiting for free: 现在,您可以将结果传递给EF或告诉编译器将其转换为代码并运行,这样您就可以免费进行短路:

Console.WriteLine(allTogether.Compile().Invoke(30)); // should be "true"

Unfortunately, this final step won't work for esoteric technical reasons. 不幸的是,由于深奥的技术原因,此最后一步无法正常工作。

But why won't it work? 但是为什么它不起作用?

Because allTogether represents an expression tree that goes somewhat like this: 因为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

Each node in the above tree represents an Expression object in the expression tree to be compiled. 上面的树中的每个节点代表要编译的表达式树中的Expression对象。 The catch is that all 4 of those PARAM(x) nodes, while logically identical are in fact different instances (that help the compiler gave us by creating expression trees automatically? well, each one naturally has their own parameter instance), while for the end result to work they must be the same instance . 问题是所有这四个PARAM(x)节点在逻辑上都是相同的,但实际上是不同的实例 (这有助于编译器通过自动创建表达式树来提供给我们?好吧,每个节点自然都有自己的参数实例),而对于工作的最终结果必须是同一实例 I know this because it has bitten me in the past . 我知道这一点,因为它过去曾刺伤我

So, what needs to be done here is that you then iterate over the resulting expression tree, find each occurrence of a ParameterExpression and replace each an every one of them with the same instance. 因此,这里需要做的是,然后遍历结果表达式树,找到每个出现的ParameterExpression并将它们中的每一个替换为相同的实例。 That same instance will also be the second argument used when constructing seed . 构造seed时,该实例也是第二个参数。

Showing how to do this will make this answer way longer than it has any right to be, but let's do it anyway. 显示如何执行此操作将使此答案的时间比其应有的权利更长,但是无论如何,让我们来做吧。 I 'm not going to comment very much, you should recognize what's going on here: 我不会在此发表过多评论,您应该了解这里发生的情况:

class Visitor : ExpressionVisitor
{
    private Expression param;

    public Visitor(Expression param)
    {
        this.param = param;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return param;
    }
}

And then: 接着:

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! 

In order to turn a sequence of Func<int, bool> objects into a bool you're going to need to have an integer to apply to each value. 为了将Func<int, bool>对象序列转换为bool您将需要有一个整数才能应用于每个值。 If you already know what that integer is, then you can do what Julien describes : 如果您已经知道该整数是什么,则可以执行Julien描述的操作

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

If you don't, then what you want to do is create a Func<int, bool> from the sequence of booleans, rather than a bool : 如果不这样做,那么您想要做的是根据布尔值而不是bool创建一个Func<int, bool>

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

We can easily generalize this into a generic function: 我们可以轻松地将其概括为通用函数:

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));
}

which allows you to write: 这使您可以编写:

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

How about this? 这个怎么样?

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));

        }

    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM