从IEnumerable <T>计算项目而不进行迭代?


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

假设我要在这些对象上进行迭代并编写类似处理#m的#n的内容。

有没有一种方法可以在我的主迭代之前不进行迭代就找出m的值?

我希望我能说清楚。

Answers:


338

IEnumerable不支持这一点。这是设计使然。IEnumerable使用惰性评估来获取您需要的元素,然后再使用它们。

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


38
如果您不需要通过索引器访问列表,则我更喜欢ICollection而不是IList。
Michael Meadows

3
我通常只是出于习惯而抓住List和IList。但是特别是如果您想自己实现它们,则ICollection更容易,并且具有Count属性。谢谢!
Mendelt

21
@Shimmy您迭代并计算元素。或者,您从为您执行此操作的Linq命名空间中调用Count()。
Mendelt

1
在正常情况下,用IList替换IEnumerable是否足够?
Teekin '04

1
@Helgi因为IEnumerable的计算是延迟的,所以您可以将其用于无法使用IList的事物。您可以构建一个返回IEnumerable的函数,该函数枚举Pi的所有小数。只要您从不尝试提出完整的结果,它就可以正常工作。您不能创建包含Pi的IList。但这全是学术性的。对于大多数正常使用,我完全同意。如果需要计数,则需要IList。:-)
Mendelt

215

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

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;

因此,它尝试强制转换为ICollection<T>具有Count属性的,并在可能的情况下使用它。否则进行迭代。

因此,最好的选择是Count()IEnumerable<T>对象上使用扩展方法,因为那样会获得最佳性能。


13
它尝试先转换为非常有趣的事情ICollection<T>
奥斯卡·梅德罗斯

1
@OscarMederos Enumerable中的大多数扩展方法都对各种类型的序列进行了优化,如果可以的话,它们将使用更便宜的方式。
Shibumi 2012年

1
提到的扩展名从.Net 3.5开始可用,并在MSDN中进行了记录
基督教徒

我认为你不需要泛型类型TIEnumerator<T> enumerator = source.GetEnumerator(),只需你可以做到这一点:IEnumerator enumerator = source.GetEnumerator()与应努力!
Jaider 2015年

7
@Jaider-比这稍微复杂一点。IEnumerable<T>继承IDisposable,使using语句可以自动处理它。IEnumerable才不是。因此,GetEnumerator无论您采用哪种方式打电话,您都应该以var d = e as IDisposable; if (d != null) d.Dispose();
Daniel Earwicker

87

只需添加一些额外的信息:

Count()扩展并不总是迭代。考虑将Linq转换为Sql,其中将计数发送到数据库,但是它不带回所有行,而是发出Sql Count()命令并返回该结果。

另外,编译器(或运行时)足够聪明,Count()如果有的话,它将调用objects 方法。因此,它并不像其他响应者所说的那样,完全无知并总是为了计数元素而反复进行。

在许多情况下,程序员只是if( enumerable.Count != 0 )使用Any()扩展方法进行检查,因为if( enumerable.Any() ) linq的惰性评估会更有效,因为一旦确定有任何元素,它就会短路。它也更具可读性


2
关于集合和数组。如果您碰巧使用了集合,请使用.Count属性,因为它始终知道其大小。查询collection.Count时没有其他计算,它仅返回已知的计数。同样的Array.length,据我所知。但是,使用.Any()获取源的枚举数,using (IEnumerator<TSource> enumerator = source.GetEnumerator())如果可以,则返回true enumerator.MoveNext()。对于collections if(collection.Count > 0),arrays if(array.length > 0)和on enumerables而言if(collection.Any())
Nope 2013年

1
第一点并非完全正确... LINQ to SQL使用此扩展方法该扩展方法此不同。如果使用秒,则计数在内存中执行,而不是作为SQL函数执行
AlexFoxGill

1
@AlexFoxGill是正确的。如果您明确将其强制转换IQueryably<T>为a IEnumerable<T>,则不会发出sql计数。当我写这篇文章时,Linq to Sql是新的。我想我只是在努力使用,Any()因为对于枚举,集合和sql而言,它的效率和可读性要高得多(通常)。感谢您改善答案。
罗伯特·保尔森

12

我的一个朋友有一系列博客文章,这些插图说明了为什么您不能这样做。他创建了一个返回IEnumerable的函数,该函数每次迭代都返回下一个素数,一直到ulong.MaxValue,直到您要求下一个项目才被计算。快速弹出的问题:退回多少物品?

这些是帖子,但篇幅较长:

  1. Beyond Loops(提供其他帖子中使用的初始EnumerableUtility类)
  2. 迭代的应用(初始实现)
  3. 疯狂的扩展方法:ToLazyList(性能优化)

2
我真的很希望MS定义了一种方法,可以让枚举对象描述他们可以对自己进行的操作(“不知道任何事情”是有效的答案)。任何枚举都不应有困难回答“您知道自己是有限的”,“您知道自己是否少于N个元素是有限的”和“您知道自己是无限的”这样的问题,因为任何枚举都可以合法地(如果没有帮助)对所有人回答“否”。如果有一种标准的方法可以问这样的问题,那么枚举者返回无尽的序列会更加安全……
supercat

1
...(因为他们可以说他们这样做了),并且代码假设不要求返回无尽序列的枚举数很可能是有界的。请注意,包括提出这样的问题的方法(以尽量减少样板,可能有一个属性返回EnumerableFeatures对象)将不需要枚举器做任何困难的事情,而是能够提出这样的问题(以及诸如“您可以保证...总是返回相同的项目序列”,“您是否可以安全地使用不应更改基础集合的代码”等)将非常有用。
2015年

那会很酷,但是我现在确定与迭代器块的啮合程度如何。您需要某种特殊的“收益率选择”或其他东西。或者,也许使用属性来装饰迭代器方法。
2015年

1
在没有任何其他特殊声明的情况下,迭代器块可以简单地报告它们对返回的序列一无所知,即使是IEnumerator或MS认可的后继者(可以由知道其存在的GetEnumerator实现返回)。为了支持其他信息,C#可能会得到一条set yield options声明或类似的内容来支持它。如果设计合理,则IEnhancedEnumerator可以消除很多“防御性” ToArrayToList呼叫,从而使LINQ之类的东西更加有用,尤其是...
supercat

...在诸如此类的情况下,Enumerable.Concat通常会将对自己有很多了解的大型收藏与不了解的小收藏结合起来。
2015年

10

IEnumerable在不进行迭代的情况下无法计数。

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

Count是一个扩展方法,在静态类Enumerable上定义。这意味着可以在IEnumerable <T>派生类的任何实例上调用它,而不管该类的实现如何。但这也意味着它是在任何这些类外部的一个地方实现的。当然,这意味着必须以完全独立于此类内部的方式来实现它。这种唯一的计数方法是通过迭代。


除非反复进行,否则无法计数是一个好方法。计数功能与实现IEnumerable的类联系在一起。因此,您必须检查IEnumerable传入的类型(通过强制转换检查),然后您知道List <>和Dictionary <>具有某些计数方法,然后使用只有在您知道类型之后。我发现此线程对个人非常有用,因此也感谢克里斯的答复。
PositiveGuy 2010年

1
根据丹尼尔(Daniel)的回答,此答案并非严格正确:实现确实检查对象是否实现了具有“ Count”字段的“ ICollection”。如果是这样,它将使用它。(我不知道08年是否那么聪明。)
ToolmakerSteve


8

不,不一般。使用枚举的一点是,枚举中的对象的实际集合是未知的(预先知道,甚至根本不知道)。


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

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 2012年

如果您有IEnumerable,.Count的实现将枚举所有项。(与ICollection不同)。OP问题显然是“没有迭代的”
JDC

5

除了您提出的紧迫问题(已完全否定回答)之外,如果您希望在处理枚举的同时报告进度,则可能需要查看我的博客文章“ Linq查询期间的进度报告”

它可以让您做到这一点:

BackgroundWorker worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.DoWork += (sender, e) =>
      {
          // pretend we have a collection of 
          // items to process
          var items = 1.To(1000);
          items
              .WithProgressReporting(progress => worker.ReportProgress(progress))
              .ForEach(item => Thread.Sleep(10)); // simulate some real work
      };

4

我在方法内部使用了这种方法来检查传入的IEnumberable内容

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

}

在这样的方法中:

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

}

1
为什么这样 “计数”可能很昂贵,因此给定IEnumerable,将所需的任何输出初始化为适当的默认值,然后开始对“ iEnum”进行迭代的成本要低一些。选择默认值,以使从不执行循环的空“ iEnum”以有效结果结束。有时,这意味着添加一个布尔标志以了解循环是否已执行。固然笨拙,但是依靠“ Count”似乎是不明智的。如果需要此标志,则代码如下所示: bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }
ToolmakerSteve

...它也很容易添加应该做的只有第一次,或者只在迭代特殊代码比第一次:`... {{如果= hasContents真实;(hasContents!)..一个时间码..; } else {除了第一次以外的所有代码。}} .Count之间()”可能是一个问题,那么这是要走的路。
ToolmakerSteve

3

这取决于.Net的版本以及IEnumerable对象的实现。Microsoft已修复IEnumerable.Count方法以检查实现,并使用ICollection.Count或ICollection <TSource> .Count,请在此处查看详细信息https://connect.microsoft.com/VisualStudio/feedback/details/454130

下面是Ildasm提供的System.Core的MSIL,System.Linq驻留在其中。

.method public hidebysig static int32  Count<TSource>(class 

[mscorlib]System.Collections.Generic.IEnumerable`1<!!TSource> source) cil managed
{
  .custom instance void System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       85 (0x55)
  .maxstack  2
  .locals init (class [mscorlib]System.Collections.Generic.ICollection`1<!!TSource> V_0,
           class [mscorlib]System.Collections.ICollection V_1,
           int32 V_2,
           class [mscorlib]System.Collections.Generic.IEnumerator`1<!!TSource> V_3)
  IL_0000:  ldarg.0
  IL_0001:  brtrue.s   IL_000e
  IL_0003:  ldstr      "source"
  IL_0008:  call       class [mscorlib]System.Exception System.Linq.Error::ArgumentNull(string)
  IL_000d:  throw
  IL_000e:  ldarg.0
  IL_000f:  isinst     class [mscorlib]System.Collections.Generic.ICollection`1<!!TSource>
  IL_0014:  stloc.0
  IL_0015:  ldloc.0
  IL_0016:  brfalse.s  IL_001f
  IL_0018:  ldloc.0
  IL_0019:  callvirt   instance int32 class [mscorlib]System.Collections.Generic.ICollection`1<!!TSource>::get_Count()
  IL_001e:  ret
  IL_001f:  ldarg.0
  IL_0020:  isinst     [mscorlib]System.Collections.ICollection
  IL_0025:  stloc.1
  IL_0026:  ldloc.1
  IL_0027:  brfalse.s  IL_0030
  IL_0029:  ldloc.1
  IL_002a:  callvirt   instance int32 [mscorlib]System.Collections.ICollection::get_Count()
  IL_002f:  ret
  IL_0030:  ldc.i4.0
  IL_0031:  stloc.2
  IL_0032:  ldarg.0
  IL_0033:  callvirt   instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<!!TSource>::GetEnumerator()
  IL_0038:  stloc.3
  .try
  {
    IL_0039:  br.s       IL_003f
    IL_003b:  ldloc.2
    IL_003c:  ldc.i4.1
    IL_003d:  add.ovf
    IL_003e:  stloc.2
    IL_003f:  ldloc.3
    IL_0040:  callvirt   instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    IL_0045:  brtrue.s   IL_003b
    IL_0047:  leave.s    IL_0053
  }  // end .try
  finally
  {
    IL_0049:  ldloc.3
    IL_004a:  brfalse.s  IL_0052
    IL_004c:  ldloc.3
    IL_004d:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0052:  endfinally
  }  // end handler
  IL_0053:  ldloc.2
  IL_0054:  ret
} // end of method Enumerable::Count


2

IEnumerable.Count()函数的结果可能是错误的。这是一个非常简单的示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections;

namespace Test
{
  class Program
  {
    static void Main(string[] args)
    {
      var test = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 };
      var result = test.Split(7);
      int cnt = 0;

      foreach (IEnumerable<int> chunk in result)
      {
        cnt = chunk.Count();
        Console.WriteLine(cnt);
      }
      cnt = result.Count();
      Console.WriteLine(cnt);
      Console.ReadLine();
    }
  }

  static class LinqExt
  {
    public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int chunkLength)
    {
      if (chunkLength <= 0)
        throw new ArgumentOutOfRangeException("chunkLength", "chunkLength must be greater than 0");

      IEnumerable<T> result = null;
      using (IEnumerator<T> enumerator = source.GetEnumerator())
      {
        while (enumerator.MoveNext())
        {
          result = GetChunk(enumerator, chunkLength);
          yield return result;
        }
      }
    }

    static IEnumerable<T> GetChunk<T>(IEnumerator<T> source, int chunkLength)
    {
      int x = chunkLength;
      do
        yield return source.Current;
      while (--x > 0 && source.MoveNext());
    }
  }
}

结果必须为(7,7,3,3),但实际结果为(7,7,3,17)


1

我发现最好的方法是通过将其转换为列表来计数。

IEnumerable<T> enumList = ReturnFromSomeFunction();

int count = new List<T>(enumList).Count;

-1

我建议致电ToList。是的,您正在尽早进行枚举,但是您仍然可以访问项目列表。


-1

它可能不会产生最佳性能,但是您可以使用LINQ来计数IEnumerable中的元素:

public int GetEnumerableCount(IEnumerable Enumerable)
{
    return (from object Item in Enumerable
            select Item).Count();
}

结果与仅执行“`return Enumerable.Count();”有何不同?
ToolmakerSteve

好问题,或者实际上是这个Stackoverflow问题的答案。
雨果


-3

我使用IEnum<string>.ToArray<string>().Length,并且效果很好。


这应该工作正常。IEnumerator的<对象> .ToArray <对象>。长度

1
你为什么这样做,而不是更简洁,更快速,执行在已经给出的解决方案丹尼尔的比你写了三年前高upvoted回答,“ IEnum<string>.Count();“?
ToolmakerSteve

你是对的。我以某种方式忽略了Daniel的答案,可能是因为他引用了实现,并且我认为他实现了扩展方法,并且我正在寻找一种使用更少代码的解决方案。
奥利弗·科特(OliverKötter),2017年

处理我的错误答案的最可接受的方法是什么?我应该删除它吗?
奥利弗·科特(OliverKötter),2017年

-3

如果我有字符串列表,则使用此类代码:

((IList<string>)Table).Count
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.