Что мне недостает в этой цепочке предикатов?
ПРИМЕЧАНИЕ: перед отправкой этого вопроса мне пришло в голову, что лучше сделать то, что я пытаюсь выполнить (и я чувствую себя довольно глупо):
IEnumerable<string> checkedItems = ProductTypesList.CheckedItems.Cast<string>();
filter = p => checkedItems.Contains(p.ProductType);
Итак, да, я уже это понимаю. Тем не менее, я публикую вопрос в любом случае, потому что я все еще не совсем понимаю, почему я (глупо) пытается делать не работал.
Я думал, что это будет очень легко. Оказывается, это дает мне большую головную боль.
Основная идея: отобразить все элементы, значение свойства ProductType
отмечено в CheckedListBox
.
Реализация:
private Func<Product, bool> GetProductTypeFilter() {
// if nothing is checked, display nothing
Func<Product, bool> filter = p => false;
foreach (string pt in ProductTypesList.CheckedItems.Cast<string>()) {
Func<Product, bool> prevFilter = filter;
filter = p => (prevFilter(p) || p.ProductType == pt);
}
return filter;
}
Однако, скажем, элементы "Equity" и "ETF" проверяются в ProductTypesList
(a CheckedListBox
). Затем по какой-то причине следующий код возвращает только продукты типа "ETF":
var filter = GetProductTypeFilter();
IEnumerable<Product> filteredProducts = allProducts.Where(filter);
Я предположил, что это могло иметь какое-то отношение к какой-то саморегуляции, где filter
установлен, по существу, сам или что-то еще. И я подумал, что, возможно, используя...
filter = new Func<Product, bool>(p => (prevFilter(p) || p.ProductType == pt));
... сделал бы трюк, но такой удачи не было. Кто-нибудь может увидеть, чего я здесь не вижу?
Ответы
Ответ 1
Я считаю, что здесь есть проблема с измененным замыканием. Параметр pt
связан с выражением лямбда, но изменяется по мере продвижения цикла. Важно понимать, когда в переменной лямбда указана переменная, а не переменная.
В циклах это имеет очень важное ветвление - потому что переменная цикла изменяется, а не переопределяется. Создавая переменную внутри цикла, вы создаете новую переменную для каждой итерации, которая затем позволяет лямбда захватывать каждый независимо.
Желаемая реализация:
foreach (string pt in ProductTypesList.CheckedItems.Cast<string>()) {
string ptCheck = pt;
Func<Product, bool> prevFilter = filter;
filter = p => (prevFilter(p) || p.ProductType == ptCheck);
}
Эрик Липперт написал об этой конкретной ситуации:
Также см. вопрос Доступ к Модифицированному закрытию (2) для хорошего объяснения того, что происходит с переменными закрытия. Также есть серия статей в блоге The Old New Thing, в которой есть интересная перспектива:
Ответ 2
Это связано с закрытием. Переменная pt всегда будет ссылаться на последнее значение цикла for.
Рассмотрим следующий пример, в котором вывод является ожидаемым, потому что он использует переменную, которая находится внутри цикла for.
public static void Main(string[] args)
{
var countries = new List<string>() { "pt", "en", "sp" };
var filter = GetFilter();
Console.WriteLine(String.Join(", ", countries.Where(filter).ToArray()));
}
private static Func<string, bool> GetFilter()
{
Func<string, bool> filter = p => false;
foreach (string pt in new string[] { "pt", "en" })
{
Func<string, bool> prevFilter = filter;
string name = pt;
filter = p => (prevFilter(p) || p == name);
}
return filter;
}
Ответ 3
Поскольку вы зацикливаете и устанавливаете тип фильтра для себя, вы устанавливаете тип продукта последним pt
в каждом случае. Это модифицированное закрытие и, поскольку оно связано с задержкой, вам нужно скопировать его в каждый цикл, например:
foreach (string pt in ProductTypesList.CheckedItems.Cast<string>()) {
var mypt = pt;
Func<Product, bool> prevFilter = filter;
filter = p => (prevFilter(p) || p.ProductType == mypt);
}
Это должно привести к правильному результату, иначе последний pt
используется для всех проверок равенства.