IEnumerable是空的吗?

57

我知道这可能在大多数情况下并不影响性能,但我讨厌获取一个 IEnumerable 并执行 .Count()的想法。是否有一个类似于 STL 的 empty() 函数的 IsEmptyNotEmpty 函数?


1
当底层集合不是ICollection<T>时,使用Count检查空值可能非常低效,因为它会在返回之前计算所有元素,而你只想知道是否至少有1个元素。 - Bear Monkey
1
可能是重复的问题:C#:检查序列是否为空的推荐方法 - nawfal
8个回答

86

您需要使用IEnumerable.Any()扩展方法(.Net Framework 3.5及以上版本)。它避免了对元素进行计数。


8
详细来说,它最多计算1个元素并且非常高效。 - Bear Monkey
3
“...并且非常高效”- 它的效率取决于具体实现。如果它是一个惰性枚举(例如使用yield的方法),那么甚至启动枚举本身都可能很昂贵。 - Joe
10
昂贵并不意味着不高效。 - Bear Monkey
1
如果它是惰性计算的可枚举类型,那么对它进行计数将会更加昂贵。在所有情况下,Any() 将比 Count() 更快地产生答案,因为它在第一个成功结果上成功(并停止处理)。 - KeithS
7
也许我漏掉了什么,但是接受的回答适用于IEnumerable<T>而不是IEnumerable,因为Any需要一个约束。 - Marcote
显示剩余4条评论

22

如果它不是通用的,那么就像这样

enumeration.Cast<object>().Any();

如果它是通用的,就像已经说过的那样,使用Enumerable的扩展。


12

无需使用LINQ,您可以进行以下操作:

bool IsEmpty(IEnumerable en)
{
    foreach(var c in en) { return false; }
    return true;
}

2
更快的方法是返回en.GetEnumerator().MoveNext(),这基本上就是Any()实现的内容。你基本上是在使用foreach做同样的事情,但是你会产生额外的成本,将第一个值分配给一个本地变量,而你永远不会使用它。 - KeithS
@KeithS:这不是很安全的做法:IEnumerable 也可以是 IDisposable(对于几个可枚举对象来说,这非常重要,例如当您在 yield 语句周围有 try-finally 时)。foreach 可以确保您正确地释放资源。顺便说一下,上面的方法与 Any() 函数基本相同。 - Ruben
好的,那么使用(iter = en.GetIterator())返回iter.MoveNext();你仍然没有像使用foreach一样分配当前值。 - KeithS
3
谢谢回答有关非泛型IEnumerable的问题! Resharper建议使用以下代码: !items.Cast<object>().Any(); - sky-dev
澄清@Ruben的评论:它是由foreach创建和处理的iterator(IEnumerator);您肯定不想处理IEnumerable本身-您的调用者可能仍然需要它。 :) - ToolmakerSteve

4
你可以使用类似 Any() 或 Count() 这样的扩展方法。 但是,Count() 比 Any() 更昂贵,因为它必须执行整个枚举,正如其他人指出的那样。
但在惰性评估的情况下(例如使用 yield 的方法),两者都可能很昂贵。例如,在以下 IEnumerable 实现中,每次调用 Any 或 Count 都将产生一个新的数据库往返成本:
IEnumerable<MyObject> GetMyObjects(...)
{
    using(IDbConnection connection = ...)
    {
         using(IDataReader reader = ...)
         {
             while(reader.Read())
             {
                 yield return GetMyObjectFromReader(reader);
             }
         }
    }
}

我认为这个道理是:
  • 如果你只有一个 IEnumerable<T>,并且想要做更多的事情(例如使用 Count 或 Any),那么请先将其转换为 List(扩展方法 ToList)。通过这种方式,您保证只枚举一次。

  • 如果您正在设计返回集合的 API,请考虑返回 ICollection<T>(甚至是 IList<T>),而不是像许多人建议的那样返回 IEnumerable<T>。通过这样做,您可以加强您的契约以保证没有惰性评估(因此没有多次评估)。

请注意,我说您应该考虑返回集合,而不是总是返回集合。像往常一样,存在权衡取舍,正如下面的评论所示。

  • @KeithS 认为您永远不应该在 DataReader 上使用 yield,虽然我从不说永远,但我会说通常一个数据访问层应该返回一个 ICollection<T> 而不是一个延迟评估的 IEnumerable<T>,原因就像 KeithS 在他的评论中给出的那样。

  • @Bear Monkey 指出,在上面的示例中实例化 List 可能是昂贵的,如果数据库返回大量记录,则这也是正确的。在某些(可能很少见)情况下,忽略 @KeithS 的建议并返回延迟评估的枚举可能是合适的,前提是消费者正在执行一些不太耗时的操作(例如生成一些聚合值)。


3
这是一个不好的示例。你绝不能在一个DataReader上使用yield,除了性能原因外(它会保持锁定更长时间,把"喷火龙"式的数据流变成一滴滴的等等)。然而,将所有数据都放入List中,然后通过yield遍历它,可以达到类似的性能,而且仍然能够说明这个问题。 - KeithS
1
在返回之前进行ToList操作,如果例如数据库返回数百万条记录,则可能比重复实例化成本更加昂贵。我宁愿返回IEnumerable<T>,并让使用者决定是否要使用ToList缓存结果。还有Keith所说的。 - Bear Monkey
@KeithS 我同意你的观点,并且出于你提到的原因和其他原因,我不会在公共API中公开这样的方法。但是这里的重点仅仅是提供一个极端情况的人工示例,即开始枚举可能比实际枚举花费更长时间。 - Joe
@Bear Monkey。“ToListing ...可能会更昂贵...如果...数据库返回数百万条记录”。我完全同意。这就是为什么我说你应该“考虑”使用ToList而不是“总是”使用ToList的原因。 - Joe

2
请记住,IEnumerable只是一个接口。它背后的实现可能因类而异(考虑Joe的示例)。扩展方法IEnumerable.Any()必须是一种通用的方法,可能不是您想要的(从性能上考虑)。Yossarian提出了一种适用于许多类的方法,但如果底层实现不使用“yield”,则仍然可能付出代价。
一般来说,如果您坚持使用包含在IEnumerable接口中的集合或数组,则Cristobalito和Yossarian可能有最好的答案。我猜内置的.Any()扩展方法就是Yossarian建议的方法。

1
在`IEnumerable`或`IEnumerable`上,不行。
但这真的没有太多意义。如果一个集合是空的,并且你尝试使用`IEnumerable`来迭代它,调用`IEnumerator.MoveNext()`将只会返回false,而不会有性能损耗。

"...在没有性能成本的情况下" - 如果它不是集合,而是惰性求值的枚举,则可能会有相当大的成本。 - Joe
1
@Joe - 但是调用IEnumerable.Any()或IEnumerable.Count()也会产生相同的成本。 - Justin Niessner
我同意。因此,在某些情况下,值得考虑实例化一个列表,以便只有一次发生这种成本。请看我的答案。 - Joe

0

你也可以编写自己的 Count 扩展方法重载,像这样:

    /// <summary>
    /// Count is at least the minimum specified.
    /// </summary>
    /// <typeparam name="TSource"></typeparam>
    /// <param name="source"></param>
    /// <param name="min"></param>
    /// <returns></returns>
    public static bool Count<TSource>(this IEnumerable<TSource> source, int min)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }
        return source.Count(min, int.MaxValue);
    }

    /// <summary>
    /// Count is between the given min and max values
    /// </summary>
    /// <typeparam name="TSource"></typeparam>
    /// <param name="source"></param>
    /// <param name="min"></param>
    /// <param name="max"></param>
    /// <returns></returns>
    public static bool Count<TSource>(this IEnumerable<TSource> source, int min, int max)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }
        if (min <= 0)
        {
            throw new ArgumentOutOfRangeException("min", "min must be a non-zero positive number");
        }
        if (max <= 0)
        {
            throw new ArgumentOutOfRangeException("max", "max must be a non-zero positive number");
        }
        if (min >= max)
            throw new ArgumentOutOfRangeException("min and max", "min must be lest than max");

        var isCollection = source as ICollection<TSource>;

        if (isCollection != null)
            return isCollection.Count >= min && isCollection.Count <= max;

        var count = 0;
        using (var enumerator = source.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                count++;
                if (count >= min && count <= max)
                    return true;
            }
        }
        return false;
    }

-1
使用Enumerable.Empty(),而不是IEnumerable.Any(),可以避免最终得到空列表。

我认为用户想要一种简单的方法来检查IEnumerable是否为空。Enumerable.Empty()返回一个IEnumerable。但是IEnumerable.Any()返回布尔值。因此,我认为与这个问题相关的IEnumerable.Any()更好。https://msdn.microsoft.com/en-us/library/bb341042(v=vs.110).aspxhttps://msdn.microsoft.com/en-us/library/bb337697(v=vs.110).aspx - Menuka Ishan

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