4

我正在将 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();

有趣的部分从 开始ToElements,我将行查找转换为我的域对象列表(详细信息:它称为 an ElementRow,后来转换为 an 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,它实际上是完整的!所以,我实际上没有分配问题。除非我接受 Jon 关于 LINQ 语句中好/坏的建议。

4

1 回答 1

7

当该语句实际被迭代时,Where 子句运行并且 temp 被填满,但 discard 再也没有被分配!

它不需要再次分配 - 将discard在调用代码中分配的现有列表将被填充。

但是,我强烈建议不要使用这种方法。在这里使用out参数确实违背了 LINQ 的精神。(如果你迭代你的结果两次,你最终会得到一个包含所有坏元素的列表两次。Ick!)

我建议在删除不良记录之前具体化查询- 然后您可以运行单独的查询:

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);

通过在 a 中实现查询List<>,您只需在 等方面处理每一行一次。当然ToRowLookupToCellLookup这确实意味着您需要有足够的内存来一次保存所有元素。有一些替代方法(例如在过滤每个坏元素时对它采取行动),但它们最终仍然可能相当脆弱。

编辑: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())
{
    ...
}

我个人并不热衷于按真/假分组 - 这感觉有点滥用通常意味着基于键的查找 - 但它肯定会起作用。

于 2013-04-05T14:26:22.200 回答