如何制作流式LINQ表达式以提供已过滤的项目以及过滤的项目?

Ant*_*ean 4 c# linq enumeration out

我正在将Excel电子表格转换为"元素"列表(这是一个域名术语).在此转换期间,我需要跳过标题行并抛出无法转换的格式错误的行.

有趣的来了.我需要捕获那些格式错误的记录,以便我可以报告它们.我构建了一个疯狂的LINQ语句(如下).这些扩展方法隐藏了OpenXml库中类型的凌乱LINQ操作.

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();
Run Code Online (Sandbox Code Playgroud)

有趣的部分开始于ToElements,我将行查找转换为我的域对象列表(详细信息:它被称为an ElementRow,后来转换为a 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);
        }
    });
}
Run Code Online (Sandbox Code Playgroud)

然后,我想删除那些不良记录(在过滤之前更容易收集所有这些记录).那个方法就是RemoveBadRecords这样开始......

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

但是,我需要报告丢弃的元素!而且我不想通过报告来混淆我的变换扩展方法.所以,我去了out参数(考虑到在匿名块中使用out param 的困难)

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;
}
Run Code Online (Sandbox Code Playgroud)

哦,瞧!我以为我是铁杆,并且会一次性工作......

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);
Run Code Online (Sandbox Code Playgroud)

但是,discard当我浏览它时,我的列表中没有任何内容!我逐步完成了代码并意识到我成功创建了一个完全流式LINQ语句.

  1. 临时列表已创建
  2. Where过滤器被分配(但尚未运行)
  3. 丢弃清单已分配
  4. 然后返回流媒体的东西

discard被重复,它不包含元素,因为内容没有迭代结束.

有没有办法使用我构建的东西来解决这个问题?我是否必须在错误记录过滤器之前或期间强制重复数据?我错过了另一种建筑吗?

一些评论

Jon提到了任务/正在/正在发生.我只是没有等待它.如果我检查discard迭代后的内容elements,实际上是完整的!所以,我实际上没有任务分配问题.除非我接受Jon关于LINQ语句中哪些好/坏的建议.

Jon*_*eet 7

当语句实际迭代时,Where子句运行并且临时填充,但丢弃从未再次分配!

它不需要再次分配 - 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);
Run Code Online (Sandbox Code Playgroud)

通过在物化查询List<>,你只有在以下方面处理每一行一次ToRowLookup,ToCellLookup等它意味着你需要有足够的内存来保存所有的元素在时间,当然.还有其他方法(例如对每个坏元素进行操作,同时对其进行过滤)但它们仍然可能最终变得相当脆弱.

编辑:Servy提到的另一个选项是使用ToLookup,它将实现并一次性分组:

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

然后你可以使用:

foreach (var goodElement in lookup[false])
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

foreach (var badElement in lookup[true])
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

请注意,这会对所有元素执行排序,无论好坏.另一种方法是从原始查询中删除顺序并使用:

foreach (var goodElement in lookup[false].OrderByCompositeKey())
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

我个人并不喜欢用真/假分组 - 感觉有点滥用通常意味着基于密钥的查找 - 但它肯定会起作用.

  • 另一个选项是`allElements.ToLookup(el => el.FormatId == 0);`那么你可以从查找好/坏项中获取true/false值,但这也会实现整个查询. (3认同)