Объединить несколько предикатов - PullRequest
31 голосов
/ 08 августа 2009

Есть ли способ в c # .NET 2.0! объединить несколько предикатов?

Допустим, у меня есть следующий код.

List<string> names = new List<string>();
names.Add("Jacob");
names.Add("Emma");
names.Add("Michael");
names.Add("Isabella");
names.Add("Ethan");
names.Add("Emily");

List<string> filteredNames = names.FindAll(StartsWithE);

static bool StartsWithE(string s)
{
    if (s.StartsWith("E"))
    {
        return true;
    }
    else
    {
        return false;
    }
}

Это дает мне:

Emma
Ethan
Emily

Так что это довольно классная штука, но я знаю, что хочу фильтровать по нескольким предикатам.

Итак, я хочу сказать что-то вроде этого:

List<string> filteredNames = names.FindAll(StartsWithE OR StartsWithI);

Для того, чтобы получить:

Emma
Isabella
Ethan
Emily

Как мне этого добиться? В настоящее время я просто дважды фильтрую полный список и впоследствии объединяю результаты. Но, к сожалению, это неэффективно, и что еще более важно, я теряю первоначальный порядок сортировки, что недопустимо в моей ситуации.

Мне также нужно иметь возможность перебирать любое количество фильтров / предикатов, поскольку их может быть довольно много.

Опять же, это должно быть решение .NET 2.0, к сожалению, я не могу использовать более новую версию фреймворка

Большое спасибо.

Ответы [ 7 ]

56 голосов
/ 08 августа 2009

Как насчет:

public static Predicate<T> Or<T>(params Predicate<T>[] predicates)
{
    return delegate (T item)
    {
        foreach (Predicate<T> predicate in predicates)
        {
            if (predicate(item))
            {
                return true;
            }
        }
        return false;
    };
}

А для полноты:

public static Predicate<T> And<T>(params Predicate<T>[] predicates)
{
    return delegate (T item)
    {
        foreach (Predicate<T> predicate in predicates)
        {
            if (!predicate(item))
            {
                return false;
            }
        }
        return true;
    };
}

Затем назовите его с помощью:

List<string> filteredNames = names.FindAll(Helpers.Or(StartsWithE, StartsWithI));

Другая альтернатива - использовать многоадресные делегаты, а затем разделять их с помощью GetInvocationList(), а затем делать то же самое. Тогда вы могли бы сделать:

List<string> filteredNames = names.FindAll(Helpers.Or(StartsWithE+StartsWithI));

Хотя я не большой поклонник последнего подхода - это похоже на злоупотребление многоадресной рассылкой.

29 голосов
/ 30 октября 2012

Полагаю, вы могли бы написать что-то вроде этого:

Func<string, bool> predicate1 = s => s.StartsWith("E");
Func<string, bool> predicate2 = s => s.StartsWith("I");
Func<string, bool> combinedOr = s => (predicate1(s) || predicate2(s));
Func<string, bool> combinedAnd = s => (predicate1(s) && predicate2(s));

... и т. Д.

5 голосов
/ 06 июня 2017

Я только недавно придумал решение, подобное этой проблеме, которое также могло бы быть полезным. Я расширил метод FindAll для списков, позволяя мне складывать предикаты в списки по мере необходимости:

public static class ExtensionMethods
{
    public static List<T> FindAll<T> (this List<T> list, List<Predicate<T>> predicates)
    {
        List<T> L = new List<T> ();
        foreach (T item in list)
        {
            bool pass = true;
            foreach (Predicate<T> p in predicates)
            {
                if (!(p (item)))
                {
                    pass = false;
                    break;
                }
            }
            if (pass) L.Add (item);
        }
        return L;
    }
}

Возвращает список только с элементами, соответствующими всем заданным предикатам. Конечно, это может быть легко изменено на ИЛИ все предикаты вместо И. Но с этим одним можно собрать довольно большое разнообразие логических комбинаций.

Использование:

{
    List<Predicate<int>> P = new List<Predicate<int>> ();
    P.Add (j => j > 100);
    P.Add (j => j % 5 == 0 || j % 7 == 0);
    P.Add (j => j < 1000);

    List<int> L = new List<int> () { 0, 1, 2, ... 999, 1000 }
    List<int> result = L.FindAll (P);

    // result will contain: 105, 110, 112, 115, 119, 120, ... 994, 995 
}
1 голос
/ 08 августа 2009

В .NET 2.0 есть анонимные делегаты, которых вы можете использовать там:

List<string> filteredNames = names.FindAll(
   delegate(string s) { return StartsWithE(s) OR StartsWithI(s); }
);

На самом деле, вы также можете использовать его для замены своих функций:

List<string> filteredNames = names.FindAll(
   delegate(string s) { return s.StartsWith("E") || s.StartsWith("I"); }
);
0 голосов
/ 11 января 2018

Я широко использовал этот шаблон с указанным выше методом массива 'params', и меня заинтриговало недавнее изучение делегата Multicast. Поскольку делегаты по своей природе поддерживают список (или многоадресную рассылку), вы можете пропустить шаблон params [] и просто предоставить один делегат вашей функции Test (). Вам нужно вызвать GetInvokationList для предоставленного предиката <>. Смотрите это: Многоадресный делегат типа Func (с возвращаемым значением)?

0 голосов
/ 08 августа 2009

Вы можете заключить метод предиката в класс и заставить конструктор принять массив строк для проверки:

class StartsWithPredicate
{
    private string[] _startStrings;
    public StartsWithPredicate(params string[] startStrings)
    {
        _startStrings = startStrings;
    }
    public bool StartsWith(string s)
    {
        foreach (var test in _startStrings)
        {
            if (s.StartsWith(test))
            {
                return true;
            }
        }
        return false;
    }
}

Тогда вы можете позвонить так:

List<string> filtered = names.FindAll((new StartsWithPredicate("E", "I")).StartsWith);

Таким образом, вы можете проверить любую комбинацию входных строк без необходимости расширять базу кода новыми вариантами метода StartsWith.

0 голосов
/ 08 августа 2009

Вы можете создать третий предикат, который внутренне ИЛИ объединяет результаты. Я думаю, что вы можете сделать это на лету, используя лямбда-выражение. Примерно так (это не лямбда-выражение, так как я не слишком хорош с этим snytax):

static bool StartsWithEorI(string s)
{
    return StartsWithE(s) || StartsWithI(s);
}
...