Объедините несколько предикатов
Вопрос
Есть ли способ в С# .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, к сожалению, я не могу использовать более новую версию платформы.
Большое спасибо.
Решение
Как насчет:
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));
Хотя я не большой поклонник последнего подхода — это похоже на злоупотребление многоадресной рассылкой.
Другие советы
Я думаю, вы могли бы написать что-то вроде этого:
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));
...и так далее.
Совсем недавно я нашел решение, похожее на эту проблему, которое также может быть полезным.Я расширил метод 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
}
В .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"); }
);
Вы можете создать третий предикат, который внутренне объединяет результаты ИЛИ.Я думаю, вы могли бы сделать это на лету, используя лямбда-выражение.Что-то вроде этого (это не лямбда-выражение, поскольку я не очень хорошо разбираюсь в этом snytax):
static bool StartsWithEorI(string s)
{
return StartsWithE(s) || StartsWithI(s);
}
Вы можете обернуть метод предиката в класс и заставить конструктор принимать массив строк для проверки:
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
метод.
Широко используя этот шаблон с приведенным выше методом массива «params», я был заинтригован, недавно узнав о делегате Multicast.Поскольку делегаты по своей сути поддерживают список (или многоадресную рассылку), вы можете пропустить шаблон params[] и просто передать один делегат в функцию Test().Вам нужно будет вызвать GetInvokationList для предоставленного Predicate<>.Видеть это: Многоадресный делегат типа Func (с возвращаемым значением)?