Contar os itens de um IEnumerable sem a iteração?
-
05-07-2019 - |
Pergunta
private IEnumerable<string> Tables
{
get
{
yield return "Foo";
yield return "Bar";
}
}
Vamos dizer que eu quero iterar sobre aqueles e escrever algo como processamento #n de # m.
Existe uma maneira que eu possa descobrir o valor de m sem iteração antes da minha iteração principal?
Espero que eu fiz claro.
Solução
IEnumerable
não suporta isso. Isso ocorre por design. IEnumerable
usa avaliação preguiçosa para obter os elementos que você pedir apenas antes de precisar deles.
Se você quer saber o número de itens sem a iteração sobre eles você pode usar ICollection<T>
, ele tem uma propriedade Count
.
Outras dicas
O método de extensão System.Linq.Enumerable.Count
em IEnumerable<T>
tem a seguinte aplicaçã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;
Assim, ele tenta elenco para ICollection<T>
, que tem uma propriedade Count
e usos que se possível. Caso contrário, ele repete.
Então, sua melhor aposta é usar o método de extensão Count()
em seu objeto IEnumerable<T>
, como você vai obter o melhor desempenho possível dessa forma.
Basta adicionar extra de algumas informações:
A extensão Count()
nem sempre iterate. Considere LINQ to SQL, onde a contagem vai para o banco de dados, mas em vez de trazer de volta todas as linhas, ele emite o comando Count()
SQL e retornos que resultam em seu lugar.
Além disso, o compilador (ou tempo de execução) é suficiente inteligente que ele irá chamar o método objetos Count()
se ele tiver um. Por isso é não como outros respondedores dizer que, sendo completamente ignorante e sempre repetindo para contar elementos.
Em muitos casos em que o programador está apenas verificando if( enumerable.Count != 0 )
usando o método de extensão Any()
, como em if( enumerable.Any() )
é muito mais eficiente com avaliação preguiçosa do LINQ, pois ele pode curto-circuito, uma vez que pode determinar existem quaisquer elementos. É também mais legível
Um amigo meu tem uma série de posts que fornecem uma ilustração para que você não pode fazer isso. Ele cria função que retorna um IEnumerable, onde cada iteração retorna o próximo número primo, todo o caminho até ulong.MaxValue
, e o próximo item não é calculado até que você perguntar para ele. Rápido, pergunta pop: quantos itens são devolvidos
Aqui estão as mensagens, mas eles são tipo de muito tempo:
- Além Loops (fornece uma classe EnumerableUtility inicial utilizado nos outros lugares)
- Aplicações de Iterate (implementação inicial)
- loucos de Extensão Métodos: ToLazyList (otimizações de desempenho)
IEnumerable não pode contar, sem iteração.
Em circunstâncias "normais", seria possível para as classes que implementam IEnumerable ou IEnumerable
Em vez disso, o conde é um método de extensão, definido na classe estática Enumerable. Isso significa que ele pode ser chamado em qualquer instância de um IEnumerable
Como alternativa, você pode fazer o seguinte:
Tables.ToList<string>().Count;
Não, não em geral. Um ponto em usar enumerables é que o conjunto real de objetos na enumeração não é conhecido (com antecedência, ou mesmo em tudo).
Você pode 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());
}
}
Você vai obter o resultado '2'.
Indo além da sua pergunta imediata (que foi completamente respondida pela negativa), se você estiver olhando para relatório de progresso enquanto o processamento de um enumeráveis, que você pode querer olhar para o meu blog Relatório de Progresso Durante Linq consultas .
Ele permite que você faça o seguinte:
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
};
Eu usei tal forma dentro de um método para verificar o passado em conteúdo IEnumberable
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
}
Isso depende de qual versão do .Net e implementação do seu objeto de IEnumerable.
Microsoft fixou o método IEnumerable.Count para verificar a implementação, e usa o ICollection.Count ou ICollection
E abaixo é o MSIL de Ildasm para System.Core, no qual reside o 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
Aqui está uma grande discussão sobre avaliação preguiçosa e adiada execução . Basicamente você tem que materializar a lista para obter esse valor.
Resultado da função IEnumerable.Count () pode estar errado. Este é um exemplo muito simples de teste:
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());
}
}
}
Resultado deve ser (7,7,3,3), mas resultado real é (7,7,3,17)
A única maneira de ter uma contagem rápida é quando a coleção original tem um indexador (como matriz). A fim de criar um código genérico com um requisito mínimo que você poderia usar IEnumerable mas se você precisa a contagem também, em seguida, a minha maneira preferida é usar essa interface:
public interface IEnumAndCount<out T> : IEnumerable<T>
{
int Count { get; }
}
Se o seu acervo original não tem qualquer indexador, a sua implementação Conde poderia iteração sobre a coleção, com o hit conhecido no desempenho O (n).
Se você não quiser usar algo semelhante ao IEnumAndCount, sua melhor aposta é ir com Linq.Count por razões dadas por Daniel Earwicker perto do topo desta questão.
Boa sorte!
Não.
Você vê que a informação disponível em qualquer lugar no código que você escreveu?
Você pode argumentar que o compilador pode "ver" que há apenas dois, mas isso significaria que seria necessário analisar cada método iterator olhando apenas para esse caso patológico específico. E mesmo que o fizesse, como você lê-lo, dados os limites de um IEnumerable?
Gostaria de sugerir chamando ToList. Sim, você está fazendo a enumeração cedo, mas você ainda tem acesso a sua lista de itens.
Pode não produzir o melhor desempenho, mas você pode usar LINQ para contar os elementos de um IEnumerable:
public int GetEnumerableCount(IEnumerable Enumerable)
{
return (from object Item in Enumerable
select Item).Count();
}
Eu uso IEnum<string>.ToArray<string>().Length
e trabalha muito bem.
Eu uso esse código, se eu tiver lista de strings:
((IList<string>)Table).Count