Как создать выражение LINQ для потоковой передачи, которое доставляет как отфильтрованные, так и отфильтрованные элементы?

Я преобразовываю электронную таблицу Excel в список «Элементов» (это термин предметной области). Во время этого преобразования мне нужно пропустить строки заголовка и выбросить искаженные строки, которые невозможно преобразовать.

А теперь самое интересное. Мне нужно захватить эти искаженные записи, чтобы я мог о них сообщить. Я построил сумасшедший оператор LINQ (ниже). Это методы расширения, скрывающие беспорядочные операции LINQ с типами из библиотеки OpenXml.

var elements = sheet
    .Rows()                          <-- BEGIN sheet data transform
    .SkipColumnHeaders()
    .ToRowLookup()
    .ToCellLookup()
    .SkipEmptyRows()                 <-- END sheet data transform
    .ToElements(strings)             <-- BEGIN domain transform
    .RemoveBadRecords(out discard)
    .OrderByCompositeKey();

Интересная часть начинается с ToElements, где я преобразовываю поиск строки в свой список объектов домена (подробности: он называется ElementRow, который позже преобразуется в Element). Плохие записи создаются только с помощью ключа (индекса строки Excel) и однозначно идентифицируются по сравнению с реальным элементом.

public static IEnumerable<ElementRow> ToElements(this IEnumerable<KeyValuePair<UInt32Value, Cell[]>> map)
{
    return map.Select(pair =>
    {
        try
        {
            return ElementRow.FromCells(pair.Key, pair.Value);
        }
        catch (Exception)
        {
            return ElementRow.BadRecord(pair.Key);
        }
    });
}

Затем я хочу удалить эти плохие записи (проще собрать все перед фильтрацией). Это метод RemoveBadRecords, который начинался так ...

public static IEnumerable<ElementRow> RemoveBadRecords(this IEnumerable<ElementRow> elements)
{
    return elements.Where(el => el.FormatId != 0);
}

Однако мне нужно сообщить об отброшенных элементах! И я не хочу запутывать свой метод расширения преобразования отчетами. Итак, я перешел к параметру out (принимая во внимание трудности использования параметра out в анонимном блоке)

public static IEnumerable<ElementRow> RemoveBadRecords(this IEnumerable<ElementRow> elements, out List<ElementRow> discard)
{
    var temp = new List<ElementRow>();
    var filtered = elements.Where(el =>
    {
        if (el.FormatId == 0) temp.Add(el);
        return el.FormatId != 0;
    });

    discard = temp;
    return filtered;
}

И вот! Я думал, что я хардкор и у меня все сработает одним выстрелом ...

var discard = new List<ElementRow>();
var elements = data
    /* snipped long LINQ statement */
    .RemoveBadRecords(out discard)
    /* snipped long LINQ statement */

discard.ForEach(el => failures.Add(el));

foreach(var el in elements) 
{ 
    /* do more work, maybe add more failures */ 
}

return new Result(elements, failures);

Но в то время, когда я просматривал его, в моем discard списке ничего не было! Я прошел через код и понял, что успешно создал полностью потоковое выражение LINQ.

  1. Временный список создан
  2. Where фильтр был назначен (но еще не запущен)
  3. И список сброса был назначен
  4. Потом потоковое вещание было возвращено

Когда был повторен discard, он не содержал элементов, потому что элементы еще не повторялись.

Есть ли способ решить эту проблему, используя созданную мной вещь? Должен ли я принудительно выполнять итерацию данных до или во время фильтрации плохих записей? Есть еще одна конструкция, которую я пропустил?

Некоторые комментарии

Джон упомянул, что задание / было / происходило. Я просто этого не ждала. Если я проверю содержимое discard после итерации elements, оно действительно заполнено! Итак, у меня на самом деле нет проблемы с назначением. Если только я не приму совет Джона о том, что хорошего / плохого должно быть в заявлении LINQ.


person Anthony Mastrean    schedule 05.04.2013    source источник


Ответы (1)


Когда оператор был фактически повторен, предложение Where было выполнено, и temp заполнился, но discard больше никогда не назначался!

Его не нужно назначать повторно - будет заполнен существующий список, который будет назначен discard в коде вызова.

Однако я бы настоятельно не рекомендовал этот подход. Использование здесь параметра out действительно противоречит духу LINQ. (Если вы дважды перебираете результаты, вы получите список, содержащий все плохие элементы дважды. Черт!)

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

var allElements = sheet
    .Rows()
    .SkipColumnHeaders()
    .ToRowLookup()
    .ToCellLookup()
    .SkipEmptyRows()
    .ToElements(strings) 
    .ToList();

var goodElements = allElements.Where(el => el.FormatId != 0)
                              .OrderByCompositeKey();

var badElements = allElements.Where(el => el.FormatId == 0);

Материализуя запрос в List<>, вы обрабатываете каждую строку только один раз с точки зрения ToRowLookup, ToCellLookup и т. Д. Это, конечно же, означает, что вам нужно иметь достаточно памяти, чтобы хранить все элементы одновременно. Существуют альтернативные подходы (например, принятие мер над каждым плохим элементом при его фильтрации), но они все равно могут оказаться довольно хрупкими.

РЕДАКТИРОВАТЬ: Другой вариант, упомянутый Servy, - использовать ToLookup, который материализуется и группируется за один раз:

var lookup = sheet
    .Rows()
    .SkipColumnHeaders()
    .ToRowLookup()
    .ToCellLookup()
    .SkipEmptyRows()
    .ToElements(strings) 
    .OrderByCompositeKey()
    .ToLookup(el => el.FormatId == 0);

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

foreach (var goodElement in lookup[false])
{
    ...
}

и

foreach (var badElement in lookup[true])
{
    ...
}

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

foreach (var goodElement in lookup[false].OrderByCompositeKey())
{
    ...
}

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

person Jon Skeet    schedule 05.04.2013
comment
Другой вариант - allElements.ToLookup(el => el.FormatId == 0);. Тогда вы можете просто получить истинное / ложное значение из поиска хороших / плохих элементов, но это также материализует весь запрос. - person Servy; 05.04.2013
comment
Эй, ты прав насчет того, что задание работает! Проблема заключалась в том, что я проверял список discard. Я обновляю вопрос по этому поводу. Спасибо за ответ, есть над чем подумать. - person Anthony Mastrean; 05.04.2013
comment
@Servy, напишите это как ответ, добавив еще несколько примеров кода. Мне это интересно. - person Anthony Mastrean; 05.04.2013
comment
@AnthonyMastrean Я не понимаю, какой еще код может понадобиться помимо того, что я уже опубликовал; и он не будет функционально отличаться от кода Джона (если только предикат Where не был дорогостоящим, поскольку я избегаю вычисления его дважды, но в данном случае это не так). - person Servy; 05.04.2013
comment
@ Серви, все в порядке, я думаю, у тебя достаточно репутации? Я просто воспринял ваш комментарий как ответ и подумал, что вы хотели бы его продвигать. Спасибо, Джон, что написал это. - person Anthony Mastrean; 05.04.2013
comment
Забавно, Джон Скит отвечает на ваш вопрос, и никто больше даже не беспокоится;) - person Anthony Mastrean; 13.08.2013