Contar os itens de um IEnumerable <T> sem iterar?

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

Digamos que eu queira iterar nelas e escrever algo como processar #n de #m.

Existe uma maneira de descobrir o valor de m sem iterar antes da minha iteração principal?

Espero ter me esclarecido.

sebagomez
fonte

Respostas:

338

IEnumerablenão suporta isso. Isso é por design. IEnumerableusa avaliação lenta para obter os elementos solicitados antes de precisar deles.

Se você deseja saber o número de itens sem iterar sobre eles, pode usar ICollection<T>, ele possui uma Countpropriedade

Mendelt
fonte
38
Eu preferiria ICollection ao IList se você não precisar acessar a lista pelo indexador.
Michael Meadows
3
Normalmente, apenas pego List e IList por hábito. Mas, especialmente se você deseja implementá-los, o ICollection é mais fácil e também possui a propriedade Count. Obrigado!
Mendelt 03/10/08
21
@Shimmy Você itera e conta os elementos. Ou você chama Count () no namespace Linq que faz isso por você.
Mendelt
1
Simplesmente substituir IEnumerable por IList deve ser suficiente em circunstâncias normais?
Teekin
1
@ Helgi Como o IEnumerable é avaliado preguiçosamente, você pode usá-lo para coisas que o IList não pode ser usado. Você pode criar uma função que retorna um IEnumerable que enumera todos os decimais de Pi, por exemplo. Contanto que você nunca tente prever o resultado completo, ele deve funcionar. Você não pode criar um IList contendo Pi. Mas isso é tudo bastante acadêmico. Para a maioria dos usos normais, concordo plenamente. Se você precisa do Count, precisa do IList. :-)
Mendelt
215

O System.Linq.Enumerable.Countmétodo de extensão on IEnumerable<T>tem a seguinte implementação:

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 isso, tenta converter para ICollection<T>, que possui uma Countpropriedade, e a usa, se possível. Caso contrário, ele itera.

Portanto, sua melhor aposta é usar o Count()método de extensão em seu IEnumerable<T>objeto, pois assim você obterá o melhor desempenho possível.

Daniel Earwicker
fonte
13
Muito interessante a coisa que ele tenta lançar ICollection<T>primeiro.
Oscar Mederos
1
@OscarMederos A maioria dos métodos de extensão do Enumerable possui otimizações para seqüências dos vários tipos em que eles usarão da maneira mais barata, se puderem.
Shibumi 26/01
1
A extensão mencionada está disponível desde .Net 3.5 e documentada no MSDN .
Christian
Eu acho que você não precisa do tipo genérico Tem IEnumerator<T> enumerator = source.GetEnumerator(), simplesmente você pode fazer isso: IEnumerator enumerator = source.GetEnumerator()e deve funcionar!
Jaider
7
@ Jaider - é um pouco mais complicado do que isso. IEnumerable<T>herda, o IDisposableque permite que a usingdeclaração a descarte automaticamente. IEnumerablenão. Então, se você chamar GetEnumeratorqualquer forma, você deve terminar comvar d = e as IDisposable; if (d != null) d.Dispose();
Daniel Earwicker
87

Apenas adicionando algumas informações extras:

A Count()extensão nem sempre itera. Considere Linq to Sql, onde a contagem vai para o banco de dados, mas em vez de retornar todas as linhas, ele emite o Count()comando Sql e retorna esse resultado.

Além disso, o compilador (ou tempo de execução) é inteligente o suficiente para chamar o Count()método de objetos se tiver um. Portanto, não é como os outros respondentes dizem, sendo completamente ignorante e sempre iterando para contar elementos.

Em muitos casos em que o programador está apenas checando if( enumerable.Count != 0 )usando o Any()método de extensão, if( enumerable.Any() ) é muito mais eficiente com a avaliação preguiçosa do linq, pois pode causar um curto-circuito quando determinar que existem elementos. Também é mais legível

Robert Paulson
fonte
2
No que diz respeito a coleções e matrizes. Se você usar uma coleção, use a .Countpropriedade, pois ela sempre sabe o tamanho. Ao consultar collection.Countnão há computação adicional, ele simplesmente retorna a contagem já conhecida. Mesmo Array.lengthaté onde eu sei. No entanto, .Any()obtém o enumerador da fonte usando using (IEnumerator<TSource> enumerator = source.GetEnumerator())e retorna true, se puder enumerator.MoveNext(). Para coleções if(collection.Count > 0):, matrizes: if(array.length > 0)e em enumeráveis, faça if(collection.Any()).
Nope
1
O primeiro ponto não é estritamente verdadeiro ... O LINQ to SQL usa esse método de extensão que não é o mesmo que este . Se você usar o segundo, a contagem é realizada em memória, e não como uma função SQL
AlexFoxGill
1
@AlexFoxGill está correto. Se você explicitamente converter seu IQueryably<T>em a, IEnumerable<T>ele não emitirá uma contagem de sql. Quando escrevi isso, o Linq to Sql era novo; Eu acho que estava pressionando para usar, Any()porque para enumeráveis, coleções e sql era muito mais eficiente e legível (em geral). Obrigado por melhorar a resposta.
Robert Paulson
12

Um amigo meu tem uma série de postagens no blog que fornecem uma ilustração do motivo pelo qual você não pode fazer isso. Ele cria uma função que retorna um IEnumerable em que cada iteração retorna o próximo número primo, até o fim ulong.MaxValue, e o próximo item não é calculado até que você o solicite. Pergunta rápida e pop: quantos itens são devolvidos?

Aqui estão as postagens, mas são longas:

  1. Beyond Loops (fornece uma classe inicial EnumerableUtility usada nas outras postagens)
  2. Aplicativos de Iterate (implementação inicial)
  3. Métodos de extensão loucos: ToLazyList (otimizações de desempenho)
Joel Coehoorn
fonte
2
Eu realmente gostaria que a MS tivesse definido uma maneira de pedir a inumeráveis ​​que descrevessem o que podiam sobre si mesmos (com "não saber nada" sendo uma resposta válida). Nenhum enumerável deve ter dificuldade em responder perguntas como "Você sabe que é finito", "Você sabe que é finito com menos de N elementos" e "Você sabe que é infinito", pois qualquer enumerável poderia legitimamente (se inútil) responda "não" a todos eles. Se houvesse um meio padrão para fazer tais perguntas, seria muito mais seguro para os enumeradores retornarem seqüências infinitas ... #
308
1
... (já que eles podem dizer que o fazem) e que o código presuma que os enumeradores que não pretendem retornar sequências infinitas provavelmente sejam limitados. Observe que incluir um meio de fazer essas perguntas (para minimizar a clichê, provavelmente uma propriedade retorna um EnumerableFeaturesobjeto) não exigiria que os enumeradores fizessem algo difícil, mas seria capaz de fazer essas perguntas (junto com outros como "Você pode prometer sempre retorne a mesma sequência de itens "," Você pode ser exposto com segurança a códigos que não devem alterar sua coleção subjacente "etc.) seria muito útil.
Supercat
Isso seria bem legal, mas agora tenho certeza de que isso combinaria com os blocos do iterador. Você precisaria de algum tipo de "opções de rendimento" especiais ou algo assim. Ou talvez use atributos para decorar o método do iterador.
Joel Coehoorn
1
Blocos do iterador poderiam, na ausência de outras declarações especiais, simplesmente reportar que eles não sabem nada sobre a sequência retornada, embora se o IEnumerator ou um sucessor aprovado pelo MS (que poderia ser retornado pelas implementações do GetEnumerator que soubessem de sua existência) fosse oferecer suporte a informações adicionais, o C # provavelmente obteria uma set yield optionsdeclaração ou algo semelhante para apoiá-lo. Se projetado corretamente, um IEnhancedEnumeratorpoderia fazer coisas como LINQ muito mais útil, eliminando um monte de "defensiva" ToArrayou ToListchamadas, especialmente ...
supercat
... nos casos em que coisas como essas Enumerable.Concatsão usadas para combinar uma coleção grande que sabe muito sobre si mesma com uma coleção pequena que não sabe.
Supercat
10

IEnumerable não pode contar sem iterar.

Em circunstâncias "normais", seria possível para as classes que implementam IEnumerable ou IEnumerable <T>, como List <T>, implementar o método Count retornando a propriedade List <T> .Count. No entanto, o método Count não é realmente um método definido na interface IEnumerable <T> ou IEnumerable. (O único que é, de fato, é GetEnumerator.) E isso significa que não é possível fornecer uma implementação específica de classe.

Em vez disso, Count é um método de extensão, definido na classe estática Enumerable. Isso significa que pode ser chamado em qualquer instância de uma classe derivada IEnumerable <T>, independentemente da implementação dessa classe. Mas também significa que é implementado em um único local, externo a qualquer uma dessas classes. O que, é claro, significa que deve ser implementado de maneira completamente independente dos internos dessas classes. A única maneira de fazer a contagem é através da iteração.

Chris Ammerman
fonte
esse é um bom argumento sobre não poder contar, a menos que você itere. A funcionalidade de contagem está vinculada às classes que implementam IEnumerable .... portanto, você deve verificar que tipo IEnumerable é recebido (verificação por conversão) e então sabe que List <> e Dictionary <> têm certas maneiras de contar e usar aqueles somente depois que você souber o tipo. Eu achei esse tópico muito útil pessoalmente, então obrigado pela sua resposta, Chris.
PositiveGuy
1
De acordo com a resposta de Daniel, essa resposta não é estritamente verdadeira: a implementação verifica se o objeto implementa "ICollection", que possui um campo "Count". Se sim, ele usa. (Se foi tão inteligente em '08, eu não sei.) #
ToolmakerSteve #
9

Como alternativa, você pode fazer o seguinte:

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

Não, não em geral. Um ponto no uso de enumeráveis ​​é que o conjunto real de objetos na enumeração não é conhecido (antecipadamente ou mesmo).

JesperE
fonte
O ponto importante que você mencionou é que, mesmo quando você obtém esse objeto IEnumerable, precisa ver se consegue convertê-lo para descobrir que tipo é. Esse é um ponto muito importante para aqueles que tentam usar mais IEnumerable como eu no meu código.
PositiveGuy
8

Você pode usar o 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());
    }
}

Você obterá o resultado '2'.

prosseek
fonte
3
Isso não funciona para a variante não-genérico IEnumerable (sem especificador de tipo)
Marcel
A implementação de .Count está enumerando todos os itens se você tiver um IEnumerable. (diferente para ICollection). A questão do OP é claramente "sem iterar"
JDC
5

Indo além da sua pergunta imediata (que foi totalmente respondida em negativo), se você estiver procurando relatar o progresso enquanto processa um enumerável, consulte a minha postagem no blog Relatando o progresso durante as consultas Linq .

Permite fazer isso:

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
fonte
4

Eu usei dessa maneira dentro de um método para verificar o IEnumberableconteúdo passado

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

}

Dentro de um método como este:

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

}
Shahidul Haque
fonte
1
Por que fazer isso? "Contagem" pode ser caro, portanto, dado um IEnumerable, é mais barato inicializar as saídas necessárias para os padrões apropriados e começar a iterar sobre "iEnum". Escolha padrões de forma que um "iEnum" vazio, que nunca execute o loop, termine com um resultado válido. Às vezes, isso significa adicionar um sinalizador booleano para saber se o loop foi executado. É claro que isso é desajeitado, mas confiar em "Conde" parece imprudente. Se precisar dessa bandeira, olhares código como: bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }.
Home
... também é fácil adicionar código especial que deve ser feito apenas na primeira vez ou apenas em iterações que não sejam a primeira: `... {if (! hasContents) {hasContents = true; ..um código de tempo ..; } else {..code pela primeira vez, exceto pela primeira vez ..} ...} "É certo que isso é mais desajeitado que sua abordagem simples, onde o código único estaria dentro do seu if, antes do loop, mas se o custo de" .Count () "pode ​​ser uma preocupação, então este é o caminho a seguir. #
ToolmakerSteve #
3

Depende da versão do .Net e da implementação do seu objeto IEnumerable. A Microsoft corrigiu o método IEnumerable.Count para verificar a implementação e usa o ICollection.Count ou ICollection <TSource> .Count, consulte os detalhes aqui https://connect.microsoft.com/VisualStudio/feedback/details/454130

E abaixo está o MSIL da Ildasm para System.Core, no qual o System.Linq reside.

.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
fonte
2

Aqui está uma ótima discussão sobre avaliação lenta e execução adiada . Basicamente, você precisa materializar a lista para obter esse valor.

JP Alioto
fonte
2

O resultado da função IEnumerable.Count () pode estar errado. Esta é uma amostra muito simples para testar:

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

O resultado deve ser (7,7,3,3), mas o resultado real é (7,7,3,17)

Roman Golubin
fonte
1

A melhor maneira que encontrei é contar convertendo-o em uma lista.

IEnumerable<T> enumList = ReturnFromSomeFunction();

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

Eu sugeriria chamar a ToList. Sim, você está fazendo a enumeração antecipada, mas ainda tem acesso à sua lista de itens.

Jonathan Allen
fonte
-1

Pode não produzir o melhor desempenho, mas você pode usar o LINQ para contar os elementos em um IEnumerable:

public int GetEnumerableCount(IEnumerable Enumerable)
{
    return (from object Item in Enumerable
            select Item).Count();
}
Hugo
fonte
Como o resultado difere de simplesmente "" retornar Enumerable.Count (); "?
Home
Boa pergunta ou é realmente uma resposta a esta pergunta do Stackoverflow.
Hugo
-1

Eu acho que a maneira mais fácil de fazer isso

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

Referência: system.linq.enumerable

Sowvik Roy
fonte
A pergunta é "Contar os itens de um IEnumerable <T> sem iterar?" Como isso responde a isso?
Enigmatividade
-3

Eu uso IEnum<string>.ToArray<string>().Lengthe funciona bem.

Oliver Kötter
fonte
Isso deve funcionar bem. IEnumerator <Object> .ToArray <Object> .Length
din
1
Por que você faz isso, em vez da solução mais concisa e de desempenho mais rápida já dada na resposta altamente votada de Daniel, escrita três anos antes da sua " IEnum<string>.Count();"?
Home
Você está certo. De alguma forma, ignorei a resposta de Daniel, talvez porque ele citou a implementação e pensei que ele implementou um método de extensão e eu estava procurando uma solução com menos código.
Oliver Kötter
Qual é a maneira mais aceita de lidar com minha resposta ruim? Devo excluí-lo?
Oliver Kötter 03/03
-3

Eu uso esse código, se eu tiver uma lista de strings:

((IList<string>)Table).Count
Estou com fome
fonte