多次枚举和使用Any()函数

7

我正在尝试弄清楚当我需要执行以下操作时,LINQ的适当约定是什么:

  • 如果有项目,则逐行打印它们
  • 如果没有项目,请打印"No items"

我认为应该这样做:

if (items.Any())
{
    foreach (string item in items)
    {
        Console.WriteLine(item);
    }
}
else
{
    Console.WriteLine("No items");
}

然而,这将在技术上违反多重枚举原则。不违反该原则的方法是:
bool any = false;
foreach (string item in items)
{
    any = true;
    Console.WriteLine(item);
}   
if (!any)
{
    Console.WriteLine("No items");
}

但显然,这种方法不够优雅。

5
我可能会使用.ToList(),但说实话,我看不出你的“不够优雅”的解决方案有什么问题。 - Stephen Kennedy
4
这似乎只是在没有明显性能问题的情况下试图进行优化。与第二个例子相比,你的第一个例子更易读且更易于维护。 - Erik Philips
3
我理解这个问题的重点是根据特定准则来确定正确性,而不是性能。该准则有多个原因,其中性能只是其中之一。在此情况下,编译器不允许避免多个枚举,这与 C# 语言规范相矛盾。因此,你关于信任编译器来处理它的评论在这里是非常不恰当的。 - user743382
2
@StephenKennedy 你是指警告吗?是的,我会毫不犹豫地忽略它。 - Erik Philips
2
如果items是由数据库支持的,那么在items.Any()返回true之后但在foreach(var item in items)开始之前,其他人可能会删除最后一个项目。这样行吗?还是你真的需要一个在线示例来证明呢? - user743382
显示剩余24条评论
2个回答

4

既然我们正在谈论LINQ,那么如何提供一个非常LINQ的解决方案呢?

foreach (var item in items.DefaultIfEmpty("No items"))
    Console.WriteLine(item);

3
@TravisJ 的确,Any 不会迭代整个序列。但它确实会迭代第一个项目。如果稍后您使用 foreach 进行迭代,则现在已经对源序列进行了两次迭代。这可能会导致各种问题。即使仅获取第一个项目,迭代源序列(甚至)可能会导致副作用,可能成本过高(许多序列需要在检索第一个项目之前完成大量工作),可能在后续迭代时产生不同的结果,或者任何其他可能性。 - Servy
1
若是这样的话,苹果和橙子就没法比较了。如果你知道这个序列只能通过复杂的操作生成,那么它应该保证有条目。此外,在这种极其复杂的可枚举情况下,很可能会有自定义实现,并且应该已经通过缓存自己的枚举器来防止出现这种情况。虽然这种方法确实是一种有效的做法,但并不是一个宏伟的改进。 - Travis J
3
@TravisJ 这个问题是在特别地询问如何解决他们所遇到的问题,而不需要对源序列进行两次迭代。 可能是因为他们知道他们有一个不能多次迭代的序列。 这种复杂情况可能不涉及任何自定义迭代器。 这些类型序列中最常见的情况是使用任何LINQ查询提供程序与数据库交互。 如果您认为EF(或基本上所有其他查询提供程序)由于未缓存每个查询而实现错误,请随时向MS提出。 - Servy
1
@TravisJ 上下文缓存行对象,我建议这不是查询结果的同一件事。 - NetMage
1
@TravisJ 正如NetMage已经告诉你的那样,它缓存对象并不意味着它缓存查询或其结果。它不会缓存查询的结果。每次迭代源序列时,它都会执行针对数据库的查询并获取更新后的结果。它可能会重复使用这些结果中的某些对象,而不是重新创建它们,但它仍然每次都在执行查询。 - Servy
显示剩余16条评论

-1

这可能是一个问题,也可能不是,这取决于您的使用情况,因为Any()会在条件满足时立即短路,这意味着整个IEnumerable不需要被枚举。

请注意下面的评论,指出了潜在的陷阱,例如实现是单向的或者代价高昂。

这里是参考来源

public static bool Any<TSource>(this IEnumerable<TSource> source) {
    if (source == null) throw Error.ArgumentNull("source");
    using (IEnumerator<TSource> e = source.GetEnumerator()) {
        if (e.MoveNext()) return true;
    }
    return false;
}

2
如果实现是仅向前并执行操作或者代价高昂的话,使用 .Any() 可能会有问题。 - Fredou
1
而 Resharper 警告说,这是因为您不知道第二个 GetEnumerator 调用(其中序列不为空)的成本会有多高。https://resharper-support.jetbrains.com/hc/en-us/community/posts/206012289-what-does-Possible-multiple-enumerations-of-IEnumerable-mean- - Stephen Kennedy
2
这绝对是一个问题。不仅因为即使 .Any() 也可能很昂贵,而且枚举两次可能会产生不同的结果。 - user743382
3
太多“如果”的问题与实际问题无关。没有明确的性能问题。没有明确的自定义列表类型。太多注释垃圾邮件。 - Erik Philips
1
这个问题特别地在询问如何避免两次迭代序列。说“不要费心”是错误的,尤其是因为它经常很重要。问题并不是在问是否重复迭代序列是一个问题,或者Any有多昂贵。它陈述了他们有一个不重复迭代序列的要求(这是一个足够常见的要求),并询问如何实现。说你不关心他们的要求不是一个答案。 - Servy
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接