不需要迭代,如何从IEnumerable<T>中计算项数?

368
private IEnumerable<string> Tables
{
    get
    {
        yield return "Foo";
        yield return "Bar";
    }
}

假设我想在迭代过程中编写类似于处理 #n of #m 的内容。

在我的主要迭代之前,有没有一种方法可以找出 m 的值?

希望我表达清楚了。

22个回答

389

IEnumerable 不支持此操作。这是有意设计的。 IEnumerable 采用惰性求值的方式,在你需要元素时才获取所要求的元素。

如果你想知道项目数量而不需要遍历它们,可以使用 ICollection<T>,它有一个 Count 属性。


44
如果您不需要通过索引器访问列表,我会更倾向于使用ICollection而不是IList。 - Michael Meadows
3
我通常出于习惯只使用List和IList。但是,如果你想自己实现它们,ICollection更容易使用,而且还有Count属性。谢谢! - Mendelt
23
你可以使用迭代的方式计算元素数量,或者调用Linq命名空间中的Count()函数来自动完成这个操作。 - Mendelt
1
在正常情况下,仅仅将IEnumerable替换为IList是否足够? - Teekin
1
@Helgi 因为IEnumerable是惰性求值的,所以你可以用它来做一些IList无法做到的事情。例如,你可以构建一个返回IEnumerable的函数,该函数枚举Pi的所有小数位。只要你不尝试对完整结果进行foreach,它就应该正常工作。你不能创建一个包含Pi的IList。但这都是相当学术的。对于大多数正常的用途,我完全同意。如果你需要Count,你需要IList。 :-) - Mendelt
显示剩余7条评论

260

IEnumerable<T>上的System.Linq.Enumerable.Count扩展方法具有以下实现:

ICollection<T> c = source as ICollection<TSource>;
if (c != null)
    return c.Count;

int result = 0;
using (IEnumerator<T> enumerator = source.GetEnumerator())
{
    while (enumerator.MoveNext())
        result++;
}
return result;
所以它尝试将其转换为拥有Count属性的ICollection<T>,如果可能的话就使用它。否则就进行迭代。
因此,最好的方法是在您的IEnumerable<T>对象上使用Count()扩展方法,这样可以获得最佳性能。

21
尝试先转换为 ICollection<T> 这一点非常有趣。 - Oscar Mederos
1
@OscarMederos Enumerable中的大多数扩展方法都针对不同类型的序列进行了优化,如果可以的话,它们将使用更便宜的方式。 - Shibumi
1
所提到的扩展自 .Net 3.5 起可用,并在 MSDN 中有文档记录。 - Christian
7
@Jaider - 这个问题稍稍有些复杂。IEnumerable<T> 继承了 IDisposable 接口,这使得使用 using 语句可以自动释放资源。但是 IEnumerable 没有继承 IDisposable 接口。因此,如果你以任何一种方式调用 GetEnumerator,最好在结束时加上 var d = e as IDisposable; if (d != null) d.Dispose(); - Daniel Earwicker
3
很遗憾,.NET没有一个中间接口,介于IEnumerable和ICollection之间,用于具有预先已知计数的IEnumerables(但不需要ICollection的其他功能)。如果需要在算法早期知道“Count”,则也没有任何方法可以知道给定任意IEnumerable是否更便宜调用IEnumerable.Count,从而迭代集合第二次,还是收集到列表中一次,以便您拥有计数,然后使用该列表。更糟糕的是,迭代两次可能会产生“副作用”。另一方面,枚举可能具有许多元素。 - ToolmakerSteve
显示剩余5条评论

100

仅添加一些额外的信息:

Count()扩展方法并不总是迭代。考虑使用 Linq to Sql 的情况,其中计数会发送到数据库,但是它不会将所有行都带回来,而是发出 Sql Count() 命令并返回结果。

此外,编译器(或运行时)足够聪明,如果对象具有Count() 方法,则会调用该方法。因此,与其他答复者所说的完全无知并始终迭代以计算元素数量不同。

在许多情况下,程序员只需使用 Any() 扩展方法来检查if( enumerable.Count != 0 ),例如if( enumerable.Any() ) 与 Linq 的惰性求值相比更加高效,一旦确定是否存在任何元素,它就可以进行短路运算。同时也更易读。


2
关于集合和数组。如果您使用的是集合,请使用 .Count 属性,因为它始终知道其大小。在查询 collection.Count 时,没有额外计算,它只是返回已知的计数。据我所知,对于 Array.length 也是如此。但是,.Any() 使用 using (IEnumerator<TSource> enumerator = source.GetEnumerator()) 获取源的枚举器,并在可以执行 enumerator.MoveNext() 时返回 true。对于集合: if(collection.Count > 0),数组: if(array.length > 0),对于可枚举的对象则使用 if(collection.Any()) - Nope
1
第一点并不完全正确... LINQ to SQL使用的是这个扩展方法,而不是这个。如果你使用第二个,计数将在内存中执行,而不是作为SQL函数。 - Alex
1
@AlexFoxGill是正确的。如果您将IQueryably<T>显式转换为IEnumerable<T>,它将不会发出SQL计数。当我写这篇文章时,Linq to Sql还很新;我认为我只是在推动使用Any(),因为对于可枚举、集合和SQL来说,它更有效率和易读(总体而言)。感谢您改进了答案。 - Robert Paulson

14

或者你可以尝试以下方法:

Tables.ToList<string>().Count;

13

我的一个朋友写了一系列博客文章,为什么你不能这样做提供了一个例子。他创建了一个函数,返回一个IEnumerable序列,每次迭代都会返回下一个质数,一直到ulong.MaxValue,并且直到你请求它时才计算下一个项。快速问题:会返回多少项?

这里是这些博客文章,但它们有点长:

  1. 超越循环(提供了其他帖子中使用的初始EnumerableUtility类)
  2. 应用程序的迭代(最初的实现)
  3. 疯狂的扩展方法:ToLazyList(性能优化)

3
我非常希望微软定义一种方法来询问可枚举对象自我描述的能力(“不知道任何东西”是一个有效的回答)。任何可枚举对象都不应该有困难回答诸如“您是否知道自己是有限的”,“您是否知道自己是具有少于N个元素的有限集”,以及“您是否知道自己是无限的”等问题,因为任何可枚举对象都可以合法地(虽然没有帮助)回答所有这些问题都是否定的。如果有一种标准的方法来询问这些问题,那么枚举器返回无限序列就会更加安全... - supercat
1
请注意,代码应该假设不声称返回无限序列的枚举器可能是有界的。请注意,包括一种询问此类问题的方法(为了最小化样板文件,可能会有一个属性返回一个“EnumerableFeatures”对象)不需要枚举器做任何困难的事情,但能够询问这些问题(以及其他一些问题,如“您能保证始终返回相同的项目序列”,“您可以安全地暴露给不应更改其基础集合的代码”等)将非常有用。 - supercat
那会很酷,但我不确定它是否与迭代器块很好地结合。你需要某种特殊的“yield选项”或类似的东西。或者也许使用属性来装饰迭代器方法。 - Joel Coehoorn
1
迭代器块在没有任何其他特殊声明的情况下,可以简单地报告它们不知道返回序列的任何信息,但是如果IEnumerator或经过MS认可的后继者(可以由GetEnumerator实现返回)支持附加信息,则C#可能会获得“set yield options”语句或类似支持它的内容。如果设计得当,IEnhancedEnumerator可以通过消除大量的“防御性”ToArray或ToList调用等方式,使诸如LINQ之类的东西更易于使用... - supercat
在那些使用 Enumerable.Concat 来合并一个大集合和一个不知道自己很多信息的小集合的情况下。 - supercat

11

使用IEnumerable无法进行计数而不迭代。

在“正常”情况下,实现IEnumerable或IEnumerable<T>的类(例如List<T>)可以通过返回List<T>.Count属性来实现Count方法。 但是,Count方法实际上不是在IEnumerable<T>或IEnumerable接口上定义的方法。(实际上,在这两个接口中仅有GetEnumerator方法被定义。)这意味着不能为其提供特定于类的实现。

相反,Count是一个扩展方法,定义在静态类Enumerable中。这意味着它可以在任何派生自IEnumerable<T>的类的任何实例上调用,而与该类的实现无关。但这也意味着它是在一个单独的位置实现的,与所有这些类的内部完全独立。当然,这意味着它必须以完全独立于这些类内部的方式实现计数。唯一的这种方式就是通过迭代。


这是一个很好的观点,关于不能计数除非你迭代。计数功能与实现IEnumerable接口的类相关联...因此,您必须检查传入的IEnumerable类型(通过强制转换进行检查),然后您就知道List<>和Dictionary<>有特定的计数方式,只有在知道类型之后才能使用它们。我个人认为这个线程非常有用,所以感谢Chris的回复。 - PositiveGuy
1
根据丹尼尔的回答,这个答案并不完全正确:实现确实会检查对象是否实现了“ICollection”,它有一个“Count”字段。如果是这样,就会使用它。(至于在'08年时是否如此聪明,我不知道。) - ToolmakerSteve

10
不,通常情况下不是这样的。使用可枚举对象的一个重要点是,实际枚举的对象集合是未知的(事先或者根本就不知道)。

你提出的重要观点是,即使你获得了那个IEnumerable对象,你也必须查看是否可以将其强制转换以确定它的类型。对于像我这样在代码中尝试使用更多IEnumerable的人来说,这是非常重要的一点。 - PositiveGuy

8
您可以使用System.Linq。
using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
    private IEnumerable<string> Tables
    {
        get {
             yield return "Foo";
             yield return "Bar";
         }
    }

    static void Main()
    {
        var x = new Test();
        Console.WriteLine(x.Tables.Count());
    }
}

您将得到结果为'2'。

3
这对于非泛型变体IEnumerable(没有类型限定符)无效。 - Marcel
.Count的实现会枚举IEnumerable中的所有项(对于ICollection不同)。OP的问题显然是“不迭代”。 - JDC

8

我认为最简单的方法是这样做

Enumerable.Count<TSource>(IEnumerable<TSource> source)

参考: system.linq.enumerable


问题是“如何在不迭代的情况下计算IEnumerable<T>中的项?”这个回答是怎样解决的? - Enigmativity
1
作者的意思是他们所写的代码中没有迭代,还是根本没有进行任何迭代。我认为这是一个有效的答案。 - nmishr

5

我在一个方法内使用了这种方式来检查传入的IEnumberable内容。

if( iEnum.Cast<Object>().Count() > 0) 
{

}

在像这样的方法内部:

GetDataTable(IEnumberable iEnum)
{  
    if (iEnum != null && iEnum.Cast<Object>().Count() > 0) //--- proceed further

}

1
为什么这样做呢?"Count"可能很昂贵,所以对于IEnumerable而言,更便宜的方法是将所需输出初始化为适当的默认值,然后开始迭代"iEnum"。选择默认值,使得空的"iEnum"不执行循环时也能得到有效结果。有时,这意味着添加一个布尔标志来知道循环是否被执行。尽管这有些笨拙,但依赖"Count"似乎是不明智的。如果需要这个标志,代码看起来像: bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... } - ToolmakerSteve
同时,添加特殊代码也很容易,这些代码只需要在第一次或者除了第一次迭代之外的迭代中执行:... { if (!hasContents) { hasContents = true; ..one time code..; } else { ..code for all but first time..} ...}" 诚然,这比您简单的方法更加繁琐,因为一次性代码需要放在if语句内,在循环之前,但是如果".Count()"的成本是一个问题,那么这就是正确的方式。 - ToolmakerSteve

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