Contare gli elementi da un IEnumerable <T> senza iterare?


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

Diciamo che voglio iterare su quelli e scrivere qualcosa come l'elaborazione di #n di #m.

C'è un modo in cui posso scoprire il valore di m senza iterare prima della mia iterazione principale?

Spero di essermi chiarito.

Risposte:


338

IEnumerablenon supporta questo. Questo è di progettazione. IEnumerableusa la valutazione pigra per ottenere gli elementi che chiedi appena prima di averne bisogno.

Se si vuole conoscere il numero di articoli senza iterazione su di loro si può usare ICollection<T>, ha una Countproprietà.


38
Preferirei ICollection su IList se non dovessi accedere all'elenco tramite l'indicizzatore.
Michael Meadows,

3
Di solito prendo semplicemente List e IList per abitudine. Ma soprattutto se si desidera implementarli da soli, ICollection è più semplice e ha anche la proprietà Count. Grazie!
Mendelt,

21
@Shimmy Fai l'iterazione e conti gli elementi. Oppure chiami Count () dallo spazio dei nomi Linq che lo fa per te.
Mendelt,

1
La semplice sostituzione di IEnumerable con IList dovrebbe essere sufficiente in circostanze normali?
Teekin,

1
@Helgi Poiché IEnumerable è valutato pigramente, puoi usarlo per cose che IList non può essere usato. È possibile creare una funzione che restituisce un IEnumerable che elenca tutti i decimali di Pi, ad esempio. Finché non provi mai a prevedere il risultato completo, dovrebbe funzionare. Non puoi creare un IList contenente Pi. Ma è tutto piuttosto accademico. Per la maggior parte degli usi normali sono completamente d'accordo. Se hai bisogno di Count hai bisogno di IList. :-)
Mendelt

215

Il System.Linq.Enumerable.Countmetodo di estensione attivo IEnumerable<T>ha la seguente implementazione:

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;

Quindi tenta di eseguire il cast su ICollection<T>, che ha una Countproprietà, e lo utilizza se possibile. Altrimenti itera.

Quindi la tua scommessa migliore è usare il Count()metodo di estensione sul tuo IEnumerable<T>oggetto, poiché otterrai le migliori prestazioni possibili in quel modo.


13
Molto interessante la cosa che cerca di lanciare per ICollection<T>prima.
Oscar Mederos,

1
@OscarMederos La maggior parte dei metodi di estensione in Enumerable ha ottimizzazioni per sequenze di vari tipi in cui useranno il modo più economico se possibile.
Shibumi,

1
L'estensione menzionata è disponibile da .Net 3.5 e documentata in MSDN .
Christian,

Penso che non è necessario il tipo generico Tsu IEnumerator<T> enumerator = source.GetEnumerator(), semplicemente si può fare questo: IEnumerator enumerator = source.GetEnumerator()e dovrebbe funzionare!
Jaider

7
@Jaider - è leggermente più complicato di così. IEnumerable<T>eredita IDisposableche consente usingall'istruzione di eliminarla automaticamente. IEnumerablenon. Quindi, se chiami in GetEnumeratorentrambi i modi, dovresti finire convar d = e as IDisposable; if (d != null) d.Dispose();
Daniel Earwicker,

87

Aggiungendo solo alcune informazioni extra:

L' Count()estensione non ripeterà sempre. Considera Linq in SQL, dove il conteggio va al database, ma invece di riportare tutte le righe, emette il Count()comando SQL e restituisce invece quel risultato.

Inoltre, il compilatore (o runtime) è abbastanza intelligente da chiamare il Count()metodo object se ne ha uno. Quindi non è come dicono gli altri soccorritori, essendo completamente ignoranti e sempre iteranti per contare gli elementi.

In molti casi in cui il programmatore sta solo controllando if( enumerable.Count != 0 )usando il Any()metodo di estensione, in quanto if( enumerable.Any() ) è molto più efficiente con la valutazione pigra di linq in quanto può cortocircuitare una volta che può determinare che ci sono elementi. È anche più leggibile


2
Per quanto riguarda le raccolte e gli array. Se ti capita di usare una collezione, usa la .Countproprietà perché sa sempre che è la dimensione. Quando si esegue una query, collection.Countnon esiste alcun calcolo aggiuntivo, restituisce semplicemente il conteggio già noto. Lo stesso per Array.lengthquanto ne so. Tuttavia, .Any()ottiene l'enumeratore della sorgente utilizzando using (IEnumerator<TSource> enumerator = source.GetEnumerator())e restituisce true se è possibile enumerator.MoveNext(). Per le raccolte if(collection.Count > 0):, matrici: if(array.length > 0)e su enumerables if(collection.Any()).
No,

1
Il primo punto non è strettamente vero ... LINQ to SQL utilizza questo metodo di estensione che non è lo stesso di questo . Se si utilizza il secondo, il conteggio viene eseguito in memoria, non come una funzione SQL
AlexFoxGill

1
@AlexFoxGill è corretto. Se si esegue esplicitamente il cast IQueryably<T>su a, IEnumerable<T>non verrà emesso un conteggio sql. Quando ho scritto questo, Linq a Sql era nuovo; Penso che stavo solo spingendo per usare Any()perché per enumerabili, collezioni e sql era molto più efficiente e leggibile (in generale). Grazie per aver migliorato la risposta.
Robert Paulson,

12

Un mio amico ha una serie di post sul blog che forniscono un'illustrazione del perché non puoi farlo. Crea una funzione che restituisce un IEnumerable in cui ogni iterazione restituisce il numero primo successivo, fino a ulong.MaxValue, e l'elemento successivo non viene calcolato fino a quando non lo chiedi. Domanda rapida e pop: quanti articoli vengono restituiti?

Ecco i post, ma sono piuttosto lunghi:

  1. Beyond Loops (fornisce una classe EnumerableUtility iniziale utilizzata negli altri post)
  2. Applicazioni di Iterate (implementazione iniziale)
  3. Metodi di estensione pazzi: ToLazyList (ottimizzazioni delle prestazioni)

2
Vorrei davvero che MS avesse definito un modo per chiedere a enumerabili di descrivere ciò che potevano su se stessi (con "non sapere nulla" che fosse una risposta valida). Nessun enumerabile dovrebbe avere difficoltà a rispondere a domande come "Conosci te stesso ad essere finito", "Conosci te stesso ad essere finito con meno di N elementi" e "Conosci te stesso ad essere infinito", dal momento che qualsiasi enumerabile potrebbe legittimamente (se inutilmente) rispondi "no" a tutti. Se esistessero mezzi standard per porre tali domande, sarebbe molto più sicuro per gli enumeratori restituire sequenze infinite ...
supercat

1
... (dal momento che potrebbero dire che lo fanno), e che il codice presuma che gli enumeratori che non pretendono di restituire sequenze infinite possano essere limitati. Si noti che includere un mezzo per porre tali domande (per ridurre al minimo la caldaia, probabilmente avere una proprietà restituire un EnumerableFeaturesoggetto) non richiederebbe agli enumeratori di fare qualcosa di difficile, ma essere in grado di porre tali domande (insieme ad altri come "Puoi promettere di restituire sempre la stessa sequenza di elementi "," Puoi essere esposto in modo sicuro al codice che non dovrebbe alterare la tua raccolta sottostante ", ecc.) sarebbe molto utile.
supercat,

Sarebbe piuttosto bello, ma ora sono sicuro di quanto bene si mescolerebbe con i blocchi iteratori. Avresti bisogno di una sorta di "opzioni di rendimento" o qualcosa di speciale. O forse usa gli attributi per decorare il metodo iteratore.
Joel Coehoorn,

1
I blocchi di Iteratore potrebbero, in assenza di altre dichiarazioni speciali, semplicemente segnalare che non sanno nulla della sequenza restituita, anche se IEnumerator o un successore approvato da MS (che potrebbe essere restituito dalle implementazioni di GetEnumerator che sapevano della sua esistenza) se supportassero ulteriori informazioni, C # probabilmente otterrebbe una set yield optionsdichiarazione o qualcosa di simile per supportarlo. Se progettato correttamente, IEnhancedEnumeratorpotrebbe rendere le cose come LINQ molto più utilizzabili eliminando molte "difese" ToArrayo ToListchiamate, specialmente ...
supercat

... nei casi in cui cose come Enumerable.Concatquelle usate per combinare una grande collezione che conosce molto su se stessa con una piccola che non lo fa.
supercat,

10

IEnumerable non può contare senza iterare.

In circostanze "normali", sarebbe possibile per le classi che implementano IEnumerable o IEnumerable <T>, come List <T>, implementare il metodo Count restituendo la proprietà List <T> .Count. Tuttavia, il metodo Count non è in realtà un metodo definito sull'interfaccia IEnumerable <T> o IEnumerable. (L'unico che in realtà è GetEnumerator.) Ciò significa che non può essere fornita un'implementazione specifica della classe.

Piuttosto, Count è un metodo di estensione, definito sulla classe statica Enumerable. Ciò significa che può essere chiamato su qualsiasi istanza di una classe derivata IEnumerable <T>, indipendentemente dall'implementazione di quella classe. Ma significa anche che è implementato in un unico posto, esterno a una di quelle classi. Il che ovviamente significa che deve essere implementato in modo completamente indipendente dagli interni di queste classi. L'unico modo per fare il conteggio è tramite iterazione.


questo è un buon punto per non essere in grado di contare se non si itera. La funzionalità di conteggio è legata alle classi che implementano IEnumerable .... quindi devi controllare quale tipo IEnumerable è in arrivo (controlla tramite casting) e poi sai che Elenco <> e Dizionario <> hanno determinati modi per contare e quindi usare quelli solo dopo aver conosciuto il tipo. Ho trovato questa discussione molto utile personalmente, quindi grazie anche per la tua risposta Chris.
Positivo

1
Secondo la risposta di Daniel questa risposta non è strettamente vera: l'implementazione verifica se l'oggetto implementa "ICollection", che ha un campo "Count". Se è così, lo usa. (Se fosse così intelligente nel '08, non lo so.)
ToolmakerSteve

9

In alternativa è possibile effettuare le seguenti operazioni:

Tables.ToList<string>().Count;

8

No, non in generale. Un punto nell'uso degli enumerabili è che l'insieme effettivo di oggetti nell'enumerazione non è noto (in anticipo o addirittura affatto).


Il punto importante che hai sollevato è che anche quando ottieni quell'oggetto IEnumerable, devi vedere se riesci a lanciarlo per capire di che tipo è. Questo è un punto molto importante per coloro che cercano di usare più IEnumerable come me nel mio codice.
Positivo

8

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

Otterrai il risultato '2'.


3
Questo non funziona per la variante non generica IEnumerable (senza identificatore di tipo)
Marcel

L'implementazione di .Count sta enumerando tutti gli elementi se si dispone di un IEnumerable. (diverso per ICollection). La domanda del PO è chiaramente "senza iterare"
JDC

5

Andando oltre la tua domanda immediata (a cui è stata data una risposta esaustiva in senso negativo), se stai cercando di segnalare i progressi mentre elabori un enumerabile, potresti voler guardare il mio post sul blog Progressi nei rapporti durante le query Linq .

Ti permette di fare questo:

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

Ho usato in questo modo un metodo per controllare il IEnumberablecontenuto passato

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

}

All'interno di un metodo come questo:

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

}

1
Perché farlo? "Count" potrebbe essere costoso, quindi, dato un IEnumerable, è più economico inizializzare qualsiasi output necessario alle impostazioni predefinite appropriate, quindi iniziare iterando su "iEnum". Scegli le impostazioni predefinite in modo tale che un "iEnum" vuoto, che non esegue mai il loop, si concluda con un risultato valido. A volte, questo significa aggiungere un flag booleano per sapere se il loop è stato eseguito. Ammesso che sia maldestro, ma fare affidamento su "Count" sembra poco saggio. Se il bisogno questo flag, sguardi codice come: bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }.
ToolmakerSteve

... è anche facile aggiungere un codice speciale che dovrebbe essere fatto solo la prima volta, o solo su iterazioni diverse dalla prima volta: `... {if (! hasContents) {hasContents = true; ..un codice temporale ..; } else {..code per tutti tranne la prima volta ..} ...} "Certo, questo è più goffo del tuo semplice approccio, in cui il codice una tantum sarebbe all'interno del tuo if, prima del ciclo, ma se il costo di" .Count () "potrebbe essere un problema, quindi questa è la strada da percorrere.
ToolmakerSteve

3

Dipende dalla versione di .Net e dall'implementazione dell'oggetto IEnumerable. Microsoft ha corretto il metodo IEnumerable.Count per verificare l'implementazione e utilizza ICollection.Count o ICollection <TSource> .Count, vedere i dettagli qui https://connect.microsoft.com/VisualStudio/feedback/details/454130

E sotto c'è MSIL di Ildasm per System.Core, in cui risiede 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

Il risultato della funzione IEnumerable.Count () potrebbe essere errato. Questo è un esempio molto semplice da testare:

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

Il risultato deve essere (7,7,3,3) ma il risultato effettivo è (7,7,3,17)


1

Il modo migliore che ho trovato è contare convertendolo in un elenco.

IEnumerable<T> enumList = ReturnFromSomeFunction();

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

-1

Suggerirei di chiamare ToList. Sì, stai eseguendo l'enumerazione in anticipo, ma hai ancora accesso al tuo elenco di elementi.


-1

Potrebbe non produrre le migliori prestazioni, ma puoi usare LINQ per contare gli elementi in un IEnumerable:

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

In che modo il risultato differisce dal semplice fare "` return Enumerable.Count (); "?
ToolmakerSteve

Buona domanda, o in realtà è una risposta a questa domanda StackOverflow.
Hugo,


-3

Io uso IEnum<string>.ToArray<string>().Lengthe funziona benissimo.


Questo dovrebbe funzionare bene. IEnumerator <Oggetto> .ToArray <Oggetto> .Lunghezza
din

1
Perché lo fai, piuttosto che la soluzione più concisa e dalle prestazioni più veloci già fornita nella risposta altamente votata di Daniel scritta tre anni prima della tua " IEnum<string>.Count();"?
ToolmakerSteve

Hai ragione. In qualche modo ho trascurato la risposta di Daniel, forse perché citava l'implementazione e pensavo che avesse implementato un metodo di estensione e stavo cercando una soluzione con meno codice.
Oliver Kötter

Qual è il modo più accettato di gestire la mia risposta negativa? Devo cancellarlo?
Oliver Kötter

-3

Uso tale codice, se ho un elenco di stringhe:

((IList<string>)Table).Count
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.