Zählen Sie die Elemente aus einem IEnumerable <T> ohne Iteration?


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

Nehmen wir an, ich möchte diese iterieren und so etwas wie die Verarbeitung von #n von #m schreiben.

Gibt es eine Möglichkeit, den Wert von m herauszufinden, ohne vor meiner Hauptiteration zu iterieren?

Ich hoffe ich habe mich klar gemacht.

Antworten:


338

IEnumerableunterstützt dies nicht. Dies ist beabsichtigt. IEnumerableverwendet die verzögerte Auswertung, um die gewünschten Elemente abzurufen, bevor Sie sie benötigen.

Wenn Sie die Anzahl der Elemente wissen möchten, ohne sie zu durchlaufen ICollection<T>, haben Sie eine CountEigenschaft.


38
Ich würde ICollection gegenüber IList bevorzugen, wenn Sie nicht per Indexer auf die Liste zugreifen müssen.
Michael Meadows

3
Normalerweise greife ich aus Gewohnheit nur zu List und IList. Vor allem aber, wenn Sie sie selbst implementieren möchten, ist ICollection einfacher und verfügt auch über die Count-Eigenschaft. Vielen Dank!
Mendelt

21
@Shimmy Sie iterieren und zählen die Elemente. Oder Sie rufen Count () aus dem Linq-Namespace auf, der dies für Sie erledigt.
Mendelt

1
Sollte es unter normalen Umständen ausreichen, IEnumerable einfach durch IList zu ersetzen?
Teekin

1
@Helgi Da IEnumerable träge ausgewertet wird, können Sie es für Dinge verwenden, die IList nicht verwenden kann. Sie können eine Funktion erstellen, die eine IEnumerable zurückgibt, die beispielsweise alle Dezimalstellen von Pi auflistet. Solange Sie nie versuchen, das gesamte Ergebnis zu ermitteln, sollte es einfach funktionieren. Sie können keine IList mit Pi erstellen. Aber das ist alles ziemlich akademisch. Für die meisten normalen Anwendungen stimme ich voll und ganz zu. Wenn Sie Count benötigen, benötigen Sie IList. :-)
Mendelt

215

Die System.Linq.Enumerable.CountErweiterungsmethode on IEnumerable<T>hat die folgende Implementierung:

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;

Also versucht es zu werfen ICollection<T>, was eine CountEigenschaft hat, und verwendet diese, wenn möglich. Andernfalls iteriert es.

Daher ist es am besten, die Count()Erweiterungsmethode für Ihr IEnumerable<T>Objekt zu verwenden, da Sie auf diese Weise die bestmögliche Leistung erzielen.


13
Sehr interessant die Sache, auf die es zuerst zu werfen versucht ICollection<T>.
Oscar Mederos

1
@OscarMederos Die meisten Erweiterungsmethoden in Enumerable verfügen über Optimierungen für Sequenzen der verschiedenen Typen, bei denen sie nach Möglichkeit billiger eingesetzt werden.
Shibumi

1
Die erwähnte Erweiterung ist seit .Net 3.5 verfügbar und in MSDN dokumentiert .
Christian

Ich denke , dass Sie nicht den generischen Typ benötigen Tauf IEnumerator<T> enumerator = source.GetEnumerator(), einfach, dies zu tun: IEnumerator enumerator = source.GetEnumerator()und sollte funktionieren!
Jaider

7
@Jaider - es ist etwas komplizierter. IEnumerable<T>erbt IDisposable, wodurch die usingAnweisung sie automatisch entsorgen kann. IEnumerablenicht. Also, wenn Sie so oder so anrufen GetEnumerator, sollten Sie mitvar d = e as IDisposable; if (d != null) d.Dispose();
Daniel Earwicker

87

Fügen Sie einfach einige zusätzliche Informationen hinzu:

Die Count()Erweiterung iteriert nicht immer. Betrachten Sie Linq to Sql, wo die Anzahl in die Datenbank geht. Anstatt jedoch alle Zeilen zurückzubringen, wird der Count()Befehl Sql ausgegeben und stattdessen das Ergebnis zurückgegeben.

Darüber hinaus ist der Compiler (oder die Laufzeit) so intelligent, dass er die Objektmethode Count()aufruft, falls vorhanden. Es ist also nicht so, wie andere Antwortende sagen, völlig ignorant zu sein und immer zu iterieren, um Elemente zu zählen.

In vielen Fällen, in denen der Programmierer nur if( enumerable.Count != 0 )mit der Any()Erweiterungsmethode prüft , if( enumerable.Any() ) ist dies bei der verzögerten Auswertung von linq weitaus effizienter, da er kurzschließen kann, sobald er feststellt, dass Elemente vorhanden sind. Es ist auch besser lesbar


2
In Bezug auf Sammlungen und Arrays. Wenn Sie zufällig eine Sammlung verwenden, verwenden Sie die .CountEigenschaft, da sie immer weiß, wie groß sie ist. Wenn bei der Abfrage collection.Countkeine zusätzliche Berechnung erfolgt, wird einfach die bereits bekannte Anzahl zurückgegeben. Das Gleiche gilt für Array.lengthsoweit ich weiß. Allerdings .Any()bekommt den enumerator der Quelle using (IEnumerator<TSource> enumerator = source.GetEnumerator())und liefert true , wenn es tun kann enumerator.MoveNext(). Für Sammlungen : if(collection.Count > 0), Arrays: if(array.length > 0)und für Aufzählungen if(collection.Any()).
Nein,

1
Der erste Punkt ist nicht ganz richtig ... LINQ to SQL verwendet diese Erweiterungsmethode , die nicht die gleichen wie diese . Wenn Sie die zweite verwenden, wird die Zählung im Speicher durchgeführt, nicht als SQL-Funktion
AlexFoxGill

1
@AlexFoxGill ist korrekt. Wenn Sie Ihre explizit IQueryably<T>in IEnumerable<T>a umwandeln, wird keine SQL-Anzahl ausgegeben. Als ich das schrieb, war Linq to Sql neu; Ich glaube, ich habe nur darauf Any()gedrängt, es zu verwenden, weil es für Aufzählungen, Sammlungen und SQL (im Allgemeinen) viel effizienter und lesbarer war. Vielen Dank für die Verbesserung der Antwort.
Robert Paulson

12

Ein Freund von mir hat eine Reihe von Blog-Posts, die veranschaulichen, warum Sie dies nicht tun können. Er erstellt eine Funktion, die eine IEnumerable zurückgibt, wobei jede Iteration die nächste Primzahl bis zu zurückgibt ulong.MaxValueund das nächste Element erst berechnet wird, wenn Sie danach fragen. Schnelle Pop-Frage: Wie viele Artikel werden zurückgegeben?

Hier sind die Beiträge, aber sie sind ziemlich lang:

  1. Beyond Loops (bietet eine anfängliche EnumerableUtility-Klasse, die in den anderen Posts verwendet wird)
  2. Anwendungen von Iterate ( Erstimplementierung )
  3. Verrückte Erweiterungsmethoden: ToLazyList (Leistungsoptimierungen)

2
Ich wünschte wirklich, MS hätte einen Weg definiert, um Enumerables zu bitten, zu beschreiben, was sie über sich selbst können (wobei "nichts wissen" eine gültige Antwort ist). Kein Aufzähler sollte Schwierigkeiten haben, Fragen wie "Weißt du, dass du endlich bist", "Weißt du, dass du endlich mit weniger als N Elementen bist" und "Weißt du, dass du unendlich bist" zu beantworten, da jeder Aufzähler legitim sein könnte (wenn nicht hilfreich) antworte allen mit "Nein". Wenn es ein Standardmittel gäbe, um solche Fragen zu stellen, wäre es für Enumeratoren viel sicherer, endlose Sequenzen zurückzugeben ...
Supercat

1
... (da sie sagen könnten, dass sie dies tun) und dass der Code davon ausgeht, dass Enumeratoren, die nicht behaupten, endlose Sequenzen zurückzugeben, wahrscheinlich begrenzt sind. Beachten Sie, dass das Einschließen eines Mittels zum Stellen solcher Fragen (um die Boilerplate zu minimieren, wahrscheinlich eine Eigenschaft hat, die ein EnumerableFeaturesObjekt zurückgibt ) nicht erfordert, dass Enumeratoren etwas Schwieriges tun, sondern in der Lage sind, solche Fragen zu stellen (zusammen mit einigen anderen wie "Können Sie es versprechen?" Geben Sie immer die gleiche Reihenfolge von Elementen zurück. "," Können Sie sicher Code ausgesetzt sein, der Ihre zugrunde liegende Sammlung nicht ändern sollte? "usw.) wäre sehr nützlich.
Supercat

Das wäre ziemlich cool, aber ich bin mir jetzt sicher, wie gut das mit Iteratorblöcken zusammenpassen würde. Sie würden eine Art spezielle "Ertragsoptionen" oder so etwas brauchen. Oder verwenden Sie Attribute, um die Iteratormethode zu dekorieren.
Joel Coehoorn

1
Iteratorblöcke könnten in Ermangelung anderer spezieller Deklarationen einfach melden, dass sie nichts über die zurückgegebene Sequenz wissen, obwohl IEnumerator oder ein von MS empfohlener Nachfolger (der von GetEnumerator-Implementierungen zurückgegeben werden könnte, die von ihrer Existenz wussten). Wenn zusätzliche Informationen unterstützt würden, würde C # wahrscheinlich eine set yield optionsErklärung oder ähnliches erhalten, um diese zu unterstützen. Wenn es richtig entworfen ist, IEnhancedEnumeratorkönnte es Dinge wie LINQ viel ToArrayToList
benutzerfreundlicher

... in Fällen, in denen Dinge wie Enumerable.Concatverwendet werden, um eine große Sammlung, die viel über sich selbst weiß, mit einer kleinen Sammlung zu kombinieren, die dies nicht tut.
Supercat

10

IEnumerable kann nicht ohne Iteration zählen.

Unter "normalen" Umständen können Klassen, die IEnumerable oder IEnumerable <T> implementieren, wie z. B. List <T>, die Count-Methode implementieren, indem sie die List <T> .Count-Eigenschaft zurückgeben. Die Count-Methode ist jedoch keine Methode, die auf der IEnumerable <T> - oder IEnumerable-Schnittstelle definiert ist. (Der einzige, der tatsächlich ist, ist GetEnumerator.) Dies bedeutet, dass keine klassenspezifische Implementierung dafür bereitgestellt werden kann.

Count it ist vielmehr eine Erweiterungsmethode, die in der statischen Klasse Enumerable definiert ist. Dies bedeutet, dass es für jede Instanz einer von IEnumerable <T> abgeleiteten Klasse aufgerufen werden kann, unabhängig von der Implementierung dieser Klasse. Es bedeutet aber auch, dass es an einem einzigen Ort außerhalb einer dieser Klassen implementiert wird. Was natürlich bedeutet, dass es auf eine Weise implementiert werden muss, die völlig unabhängig von den Interna dieser Klasse ist. Die einzige Möglichkeit zum Zählen ist die Iteration.


Das ist ein guter Punkt, wenn Sie nicht zählen können, wenn Sie nicht iterieren. Die Zählfunktion ist an die Klassen gebunden, die IEnumerable implementieren. Daher müssen Sie überprüfen, welcher Typ IEnumerable eingeht (durch Casting überprüfen), und dann wissen Sie, dass List <> und Dictionary <> bestimmte Möglichkeiten zum Zählen und anschließenden Verwenden haben diese erst, nachdem Sie den Typ kennen. Ich fand diesen Thread persönlich sehr nützlich, also danke auch für deine Antwort, Chris.
PositiveGuy

1
Nach Daniels Antwort ist diese Antwort nicht unbedingt richtig: Die Implementierung prüft, ob das Objekt "ICollection" implementiert, das ein "Count" -Feld hat. Wenn ja, wird es verwendet. (Ob es in '08 so schlau war, weiß ich nicht.)
ToolmakerSteve


8

Nein, im Allgemeinen nicht. Ein Punkt bei der Verwendung von Aufzählungszeichen ist, dass die tatsächliche Menge von Objekten in der Aufzählung nicht bekannt ist (im Voraus oder überhaupt nicht).


Der wichtige Punkt, den Sie angesprochen haben, ist, dass Sie selbst dann, wenn Sie dieses IEnumerable-Objekt erhalten, prüfen müssen, ob Sie es umwandeln können, um herauszufinden, um welchen Typ es sich handelt. Das ist ein sehr wichtiger Punkt für diejenigen, die versuchen, mehr IEnumerable wie mich in meinem Code zu verwenden.
PositiveGuy

8

Sie können System.Linq verwenden.

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

Sie erhalten das Ergebnis '2'.


3
Dies funktioniert nicht für die nicht generische Variante IEnumerable (ohne Typbezeichner)
Marcel

Bei der Implementierung von .Count werden alle Elemente aufgelistet, wenn Sie über eine IEnumerable verfügen. (anders für ICollection). Die OP-Frage ist eindeutig "ohne Iteration"
JDC

5

Wenn Sie über Ihre unmittelbare Frage hinausgehen (die gründlich verneint wurde), während Sie den Fortschritt während der Verarbeitung einer Aufzählung melden möchten, sollten Sie sich meinen Blog-Beitrag " Fortschritte bei Linq-Abfragen melden" ansehen .

Damit können Sie Folgendes tun:

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

Ich habe diese Methode in einer Methode verwendet, um den übergebenen IEnumberableInhalt zu überprüfen

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

}

Innerhalb einer Methode wie dieser:

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

}

1
Warum das tun? "Count" kann teuer sein. Bei einer IEnumerable ist es daher billiger, alle Ausgaben zu initialisieren, die Sie für die entsprechenden Standardeinstellungen benötigen, und dann über "iEnum" zu iterieren. Wählen Sie die Standardeinstellungen so, dass ein leeres "iEnum", das die Schleife niemals ausführt, ein gültiges Ergebnis liefert. Manchmal bedeutet dies, ein boolesches Flag hinzuzufügen, um zu wissen, ob die Schleife ausgeführt wurde. Zugegeben, das ist ungeschickt, aber sich auf "Count" zu verlassen, scheint unklug. Wenn Sie dieses Flag benötigen, sieht der Code folgendermaßen aus : bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }.
ToolmakerSteve

... es ist auch einfach, speziellen Code hinzuzufügen, der nur beim ersten Mal oder nur bei anderen Iterationen als beim ersten Mal ausgeführt werden sollte: `... {if (! hasContents) {hasContents = true; ..ein Zeitcode ..; } else {..code für alle außer dem ersten Mal ..} ...} "Zugegeben, dies ist umständlicher als Ihr einfacher Ansatz, bei dem sich ein einmaliger Code in Ihrem if befindet, vor der Schleife, aber wenn die Kosten von" .Count () "könnte ein Problem sein, dann ist dies der richtige Weg.
ToolmakerSteve

3

Dies hängt von der Version von .Net und der Implementierung Ihres IEnumerable-Objekts ab. Microsoft hat die IEnumerable.Count-Methode behoben, um nach der Implementierung zu suchen, und verwendet ICollection.Count oder ICollection <TSource> .Count. Weitere Informationen finden Sie hier https://connect.microsoft.com/VisualStudio/feedback/details/454130

Und unten ist die MSIL von Ildasm für System.Core, in der sich die System.Linq befindet.

.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

Das Ergebnis der Funktion IEnumerable.Count () ist möglicherweise falsch. Dies ist ein sehr einfach zu testendes Beispiel:

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

Das Ergebnis muss (7,7,3,3) sein, aber das tatsächliche Ergebnis ist (7,7,3,17).


1

Der beste Weg, den ich gefunden habe, ist zu zählen, indem ich ihn in eine Liste konvertiere.

IEnumerable<T> enumList = ReturnFromSomeFunction();

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

-1

Ich würde vorschlagen, ToList anzurufen. Ja, Sie führen die Aufzählung frühzeitig durch, haben jedoch weiterhin Zugriff auf Ihre Elementliste.


-1

Möglicherweise wird nicht die beste Leistung erzielt, aber Sie können LINQ verwenden, um die Elemente in einer IEnumerable zu zählen:

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

Wie unterscheidet sich das Ergebnis von "` return Enumerable.Count (); "?
ToolmakerSteve

Gute Frage, oder ist es tatsächlich eine Antwort auf diese Stackoverflow-Frage?
Hugo


-3

Ich benutze IEnum<string>.ToArray<string>().Lengthund es funktioniert gut.


Dies sollte gut funktionieren. IEnumerator <Objekt> .ToArray <Objekt> .Länge
Din

1
Warum tun Sie dies, anstatt die präzisere und schnellere Lösung zu finden, die bereits in Daniels hochkarätiger Antwort enthalten ist, die drei Jahre früher als Ihre geschrieben wurde IEnum<string>.Count();? " "?
ToolmakerSteve

Du hast recht. Irgendwie habe ich Daniels Antwort übersehen, vielleicht weil er aus der Implementierung zitiert hat und ich dachte, dass er eine Erweiterungsmethode implementiert hat und ich nach einer Lösung mit weniger Code gesucht habe.
Oliver Kötter

Was ist die am meisten akzeptierte Art, mit meiner schlechten Antwort umzugehen? Soll ich es löschen?
Oliver Kötter

-3

Ich verwende solchen Code, wenn ich eine Liste von Zeichenfolgen habe:

((IList<string>)Table).Count
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.