¿Cuenta los elementos de un IEnumerable <T> sin iterar?

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

Digamos que quiero iterar sobre ellos y escribir algo como procesar #n de #m.

¿Hay alguna manera de averiguar el valor de m sin iterar antes de mi iteración principal?

Espero haberme aclarado.

sebagomez
fuente

Respuestas:

338

IEnumerableNo es compatible con esto. Esto es por diseño. IEnumerableutiliza la evaluación diferida para obtener los elementos que solicita justo antes de que los necesite.

Si desea saber la cantidad de elementos sin iterar sobre ellos, puede usarlos ICollection<T>, tiene una Countpropiedad.

Mendelt
fuente
38
Yo preferiría ICollection sobre IList si no necesita acceder a la lista por indexador.
Michael Meadows
3
Por lo general, solo tomo List e IList por costumbre. Pero especialmente si desea implementarlos usted mismo, ICollection es más fácil y también tiene la propiedad Count. ¡Gracias!
Mendelt
21
@Shimmy Usted itera y cuenta los elementos. O puede llamar a Count () desde el espacio de nombres de Linq que hace esto por usted.
Mendelt
1
¿Simplemente reemplazar IEnumerable con IList debería ser suficiente en circunstancias normales?
Teekin
1
@Helgi Debido a que IEnumerable se evalúa perezosamente, puede usarlo para cosas que IList no puede usar. Puede crear una función que devuelva un IEnumerable que enumere todos los decimales de Pi, por ejemplo. Siempre y cuando nunca intentes predecir el resultado completo, debería funcionar. No puede hacer una IList que contenga Pi. Pero eso es todo bastante académico. Para la mayoría de los usos normales, estoy completamente de acuerdo. Si necesita Count necesita IList. :-)
Mendelt
215

El System.Linq.Enumerable.Countmétodo de extensión en IEnumerable<T>tiene la siguiente implementación:

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;

Por lo tanto, trata de enviar a ICollection<T>, que tiene una Countpropiedad, y la usa si es posible. De lo contrario, itera.

Por lo tanto, su mejor opción es utilizar el Count()método de extensión en su IEnumerable<T>objeto, ya que obtendrá el mejor rendimiento posible de esa manera.

Daniel Earwicker
fuente
13
Muy interesante lo que intenta lanzar ICollection<T>primero.
Oscar Mederos
1
@OscarMederos La mayoría de los métodos de extensión en Enumerable tienen optimizaciones para secuencias de varios tipos en los que utilizarán la forma más económica si pueden.
Shibumi
1
La extensión mencionada está disponible desde .Net 3.5 y está documentada en MSDN .
Christian
Creo que no es necesario el tipo genérico Tde IEnumerator<T> enumerator = source.GetEnumerator(), simplemente se puede hacer esto: IEnumerator enumerator = source.GetEnumerator()y debe trabajar!
Jaider
77
@Jaider: es un poco más complicado que eso. IEnumerable<T>hereda, lo IDisposableque permite que la usinginstrucción lo elimine automáticamente. IEnumerableno. Así que si usted llama GetEnumeratorde cualquier manera, usted debe terminar convar d = e as IDisposable; if (d != null) d.Dispose();
Daniel Earwicker
87

Solo agrego información adicional:

La Count()extensión no siempre itera. Considere Linq to Sql, donde el recuento va a la base de datos, pero en lugar de recuperar todas las filas, emite el Count()comando Sql y devuelve ese resultado.

Además, el compilador (o tiempo de ejecución) es lo suficientemente inteligente como para llamar al Count()método de los objetos si tiene uno. Entonces, no es como dicen otros respondedores, siendo completamente ignorantes y siempre iterando para contar elementos.

En muchos casos, el programador solo está verificando el if( enumerable.Count != 0 )uso del Any()método de extensión, ya que if( enumerable.Any() ) es mucho más eficiente con la evaluación diferida de linq, ya que puede causar un cortocircuito una vez que puede determinar que hay elementos. También es más legible

Robert Paulson
fuente
2
En lo que respecta a colecciones y matrices. Si utiliza una colección, use la .Countpropiedad, ya que siempre sabe su tamaño. Al consultar collection.Countno hay cálculo adicional, simplemente devuelve el recuento ya conocido. Lo mismo por Array.lengthlo que yo sé. Sin embargo, .Any()obtiene el enumerador de la fuente usando using (IEnumerator<TSource> enumerator = source.GetEnumerator())y devuelve verdadero si puede hacerlo enumerator.MoveNext(). Para colecciones:, if(collection.Count > 0)matrices: if(array.length > 0)y en enumerables if(collection.Any()).
No
1
El primer punto no es estrictamente cierto ... LINQ to SQL usa este método de extensión que no es el mismo que este . Si usa el segundo, el recuento se realiza en la memoria, no como una función SQL
AlexFoxGill
1
@AlexFoxGill es correcto. Si expulsa explícitamente su IQueryably<T>a IEnumerable<T>, no emitirá un recuento de sql. Cuando escribí esto, Linq to Sql era nuevo; Creo que solo estaba presionando para usar Any()porque para enumerables, colecciones y sql era mucho más eficiente y legible (en general). Gracias por mejorar la respuesta.
Robert Paulson el
12

Un amigo mío tiene una serie de publicaciones en el blog que proporcionan una ilustración de por qué no puedes hacer esto. Crea una función que devuelve un IEnumerable donde cada iteración devuelve el siguiente número primo, hasta el final ulong.MaxValue, y el siguiente elemento no se calcula hasta que lo solicite. Pregunta rápida y emergente: ¿cuántos artículos se devuelven?

Aquí están las publicaciones, pero son algo largas:

  1. Beyond Loops (proporciona una clase inicial de EnumerableUtility utilizada en las otras publicaciones)
  2. Aplicaciones de Iterate (implementación inicial)
  3. Métodos de extensión locos: ToLazyList (optimizaciones de rendimiento)
Joel Coehoorn
fuente
2
Realmente desearía que MS hubiera definido una forma de pedirles a los enumerables que describan lo que pueden sobre sí mismos (con "no saber nada" como una respuesta válida). Ningún enumerable debería tener dificultades para responder preguntas como "¿Sabe que es finito?", "¿Se sabe que es finito con menos de N elementos?" Y "¿Sabe que es infinito?", Ya que cualquier enumerable podría legítimamente (si no ayuda) responda "no" a todos ellos. Si hubiera un medio estándar para hacer tales preguntas, sería mucho más seguro para los enumeradores devolver secuencias interminables ...
supercat
1
... (ya que podrían decir que lo hacen), y para que el código asuma que los enumeradores que no afirman devolver secuencias interminables probablemente estén limitados. Tenga en cuenta que incluir un medio para hacer tales preguntas (para minimizar la repetitiva, probablemente hacer que una propiedad devuelva un EnumerableFeaturesobjeto) no requeriría que los enumeradores hicieran nada difícil, pero poder hacer tales preguntas (junto con algunas otras como "¿Puede prometerle? devolver siempre la misma secuencia de elementos "," ¿Puede estar expuesto de forma segura al código que no debería alterar su colección subyacente ", etc.) sería muy útil.
supercat
Eso sería genial, pero ahora estoy seguro de qué tan bien encajaría con los bloques iteradores. Necesitaría algún tipo de "opciones de rendimiento" especiales o algo así. O tal vez use atributos para decorar el método iterador.
Joel Coehoorn
1
En ausencia de cualquier otra declaración especial, los bloques de iterador podrían simplemente informar que no saben nada sobre la secuencia devuelta, aunque si IEnumerator o un sucesor respaldado por MS (que podría ser devuelto por implementaciones de GetEnumerator que sabían de su existencia) si admitieran información adicional, C # probablemente obtendría una set yield optionsdeclaración o algo similar para respaldarla. Si se diseña adecuadamente, IEnhancedEnumeratorpodría hacer que cosas como LINQ sean mucho más útiles al eliminar muchas "defensivas" ToArrayo ToListllamadas, especialmente ...
supercat
... en los casos en que cosas como Enumerable.Concatse usan para combinar una gran colección que sabe mucho de sí misma con una pequeña que no.
supercat
10

IEnumerable no puede contar sin iterar.

En circunstancias "normales", sería posible que las clases que implementan IEnumerable o IEnumerable <T>, como List <T>, implementen el método Count al devolver la propiedad List <T> .Count. Sin embargo, el método Count no es realmente un método definido en la interfaz IEnumerable <T> o IEnumerable. (El único que, de hecho, es GetEnumerator). Y esto significa que no se puede proporcionar una implementación específica de clase.

Más bien, Count es un método de extensión, definido en la clase estática Enumerable. Esto significa que se puede invocar en cualquier instancia de una clase derivada IEnumerable <T>, independientemente de la implementación de esa clase. Pero también significa que se implementa en un solo lugar, externo a cualquiera de esas clases. Lo que, por supuesto, significa que debe implementarse de una manera que sea completamente independiente de las partes internas de estas clases. La única forma de contar es a través de la iteración.

Chris Ammerman
fuente
ese es un buen punto sobre no poder contar a menos que itere. La funcionalidad de recuento está vinculada a las clases que implementan IEnumerable ... por lo tanto, debe verificar qué tipo de IEnumerable es entrante (verificar mediante conversión) y luego sabe que List <> y Dictionary <> tienen ciertas formas de contar y luego usar solo después de que sepas el tipo. Este hilo me pareció muy útil personalmente, así que gracias por tu respuesta también Chris.
PositiveGuy
1
Según la respuesta de Daniel, esta respuesta no es estrictamente cierta: la implementación comprueba si el objeto implementa "ICollection", que tiene un campo "Count". Si es así, lo usa. (Si fue tan inteligente en el '08, no lo sé).
ToolmakerSteve
9

Alternativamente, puede hacer lo siguiente:

Tables.ToList<string>().Count;
Anatoliy Nikolaev
fuente
8

No, no en general. Un punto en el uso de enumerables es que no se conoce el conjunto real de objetos en la enumeración (de antemano, o incluso en absoluto).

JesperE
fuente
El punto importante que mencionó es que incluso cuando obtiene ese objeto IEnumerable, debe ver si puede lanzarlo para descubrir de qué tipo es. Ese es un punto muy importante para aquellos que intentan usar más IEnumerable como yo en mi código.
PositiveGuy
8

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

Obtendrás el resultado '2'.

prosseek
fuente
3
Esto no funciona para la variante no genérica IEnumerable (sin especificador de tipo)
Marcel
La implementación de .Count enumera todos los elementos si tiene un IEnumerable. (diferente para ICollection). La pregunta de OP es claramente "sin iterar"
JDC
5

Yendo más allá de su pregunta inmediata (que ha sido completamente respondida en forma negativa), si está buscando informar sobre el progreso mientras procesa una enumerable, es posible que desee ver mi publicación de blog Informe de progreso durante las consultas de Linq .

Te permite hacer esto:

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
      };
Samuel Jack
fuente
4

Utilicé tal método dentro de un método para verificar el IEnumberablecontenido pasado

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

}

Dentro de un método como este:

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

}
Shahidul Haque
fuente
1
¿Por qué hacer eso? "Contar" puede ser costoso, por lo tanto, dado un IEnumerable, es más barato inicializar cualquier salida que necesite para los valores predeterminados apropiados, luego comenzar a iterar sobre "iEnum". Elija valores predeterminados de modo que un "iEnum" vacío, que nunca ejecuta el ciclo, termine con un resultado válido. A veces, esto significa agregar una bandera booleana para saber si el bucle se ejecutó. Es cierto que es torpe, pero confiar en "Count" parece imprudente. Si necesita este indicador, se ve un código como: bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }.
ToolmakerSteve
... también es fácil agregar un código especial que debe hacerse solo la primera vez, o solo en iteraciones que no sean la primera vez: `... {if (! hasContents) {hasContents = true; ..código de una sola vez..; } else {..code for all but first time ..} ...} "Es cierto que esto es más torpe que su enfoque simple, donde el código de una sola vez estaría dentro de su if, antes del ciclo, pero si el costo de" .Count () "podría ser una preocupación, entonces este es el camino a seguir.
ToolmakerSteve
3

Depende de la versión de .Net y la implementación de su objeto IEnumerable. Microsoft ha reparado el método IEnumerable.Count para verificar la implementación, y utiliza ICollection.Count o ICollection <TSource> .Count, vea los detalles aquí https://connect.microsoft.com/VisualStudio/feedback/details/454130

Y a continuación se muestra el MSIL de Ildasm para System.Core, en el que reside 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
prabug
fuente
2

El resultado de la función IEnumerable.Count () puede ser incorrecto. Esta es una muestra muy simple para probar:

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

El resultado debe ser (7,7,3,3) pero el resultado real es (7,7,3,17)

Golubin romano
fuente
1

La mejor manera que encontré es contar convirtiéndola en una lista.

IEnumerable<T> enumList = ReturnFromSomeFunction();

int count = new List<T>(enumList).Count;
Abhas Bhoi
fuente
-1

Sugeriría llamar a ToList. Sí, está haciendo la enumeración temprano, pero aún tiene acceso a su lista de elementos.

Jonathan Allen
fuente
-1

Es posible que no produzca el mejor rendimiento, pero puede usar LINQ para contar los elementos en un IEnumerable:

public int GetEnumerableCount(IEnumerable Enumerable)
{
    return (from object Item in Enumerable
            select Item).Count();
}
Hugo
fuente
¿Cómo difiere el resultado de simplemente hacer "` return Enumerable.Count (); "?
ToolmakerSteve
Buena pregunta, o es realmente una respuesta a esta pregunta de Stackoverflow.
Hugo
-1

Creo que la forma más fácil de hacer esto.

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

Referencia: system.linq.enumerable

Sowvik Roy
fuente
La pregunta es "¿Cuenta los elementos de un IEnumerable <T> sin iterar?" ¿Cómo responde esto a eso?
Enigmatividad
-3

Yo uso IEnum<string>.ToArray<string>().Lengthy funciona bien.

Oliver Kötter
fuente
Esto debería funcionar bien. IEnumerator <Object> .ToArray <Object> .Length
din
1
¿Por qué haces esto, en lugar de la solución más concisa y de más rápido rendimiento ya dada en la respuesta altamente votada de Daniel escrita tres años antes que la tuya , " IEnum<string>.Count();"?
ToolmakerSteve
Tienes razón. De alguna manera pasé por alto la respuesta de Daniel, tal vez porque citó la implementación y pensé que implementó un método de extensión y estaba buscando una solución con menos código.
Oliver Kötter
¿Cuál es la forma más aceptada de manejar mi mala respuesta? ¿Debo eliminarlo?
Oliver Kötter
-3

Uso dicho código, si tengo una lista de cadenas:

((IList<string>)Table).Count
Tengo hambre
fuente