Что мне недостает в этой цепочке предикатов?

ПРИМЕЧАНИЕ: перед отправкой этого вопроса мне пришло в голову, что лучше сделать то, что я пытаюсь выполнить (и я чувствую себя довольно глупо):

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 используется для всех проверок равенства.