Conta gli elementi da un IEnumerable < T > senza iterare?
-
05-07-2019 - |
Domanda
private IEnumerable<string> Tables
{
get
{
yield return "Foo";
yield return "Bar";
}
}
Diciamo che voglio iterare su quelli e scrivere qualcosa come l'elaborazione #n di #m.
Esiste un modo per scoprire il valore di m senza iterare prima della mia iterazione principale?
Spero di essermi chiarito.
Soluzione
IEnumerable
non supporta questo. Questo è di progettazione. IEnumerable
utilizza la valutazione lazy per ottenere gli elementi richiesti appena prima di averne bisogno.
Se vuoi conoscere il numero di elementi senza iterare su di essi puoi usare ICollection < T >
, ha una proprietà Count
.
Altri suggerimenti
Il metodo di estensione System.Linq.Enumerable.Count
su 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 proprietà Count
e la usa se possibile. Altrimenti itera.
Quindi la soluzione migliore è utilizzare il metodo di estensione Count ()
sull'oggetto IEnumerable < T >
, poiché otterrai le migliori prestazioni possibili in questo modo.
Aggiungendo solo alcune informazioni extra:
L'estensione Count ()
non scorre sempre. Si consideri Linq a Sql, dove il conteggio va al database, ma invece di riportare tutte le righe, emette il comando Sql Count ()
e restituisce invece quel risultato.
Inoltre, il compilatore (o runtime) è abbastanza intelligente da chiamare il metodo Count ()
degli oggetti se ne ha uno. Quindi è non come dicono gli altri rispondenti, essendo completamente ignoranti e sempre iteranti per contare gli elementi.
In molti casi in cui il programmatore sta solo controllando if (enumerable.Count! = 0)
utilizzando il metodo di estensione Any ()
, come in 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
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:
- Beyond Loops (fornisce una classe EnumerableUtility iniziale utilizzata negli altri post)
- Applicazioni di Iterate (Implementazione iniziale)
- Metodi di estensione pazza: ToLazyList (ottimizzazioni delle prestazioni)
IEnumerable non può contare senza iterare.
In " normale " circostanze, 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 su IEnumerable < T > o interfaccia IEnumerable. (L'unico che in realtà è GetEnumerator). Ciò significa che non è possibile fornire un'implementazione specifica per classe.
Piuttosto, Count è un metodo di estensione, definito sulla classe statica Enumerable. Ciò significa che può essere chiamato in qualsiasi istanza di un IEnumerable < T > classe derivata, 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 contare è tramite iterazione.
In alternativa puoi fare quanto segue:
Tables.ToList<string>().Count;
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).
Puoi usare 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'.
Andando oltre la tua domanda immediata (a cui è stata data una risposta esauriente in negativo), se stai cercando di segnalare i progressi durante l'elaborazione di un enumerabile, potresti voler guardare il mio post sul blog Avanzamento dei report durante le query Linq .
Ti consente di farlo:
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
};
Ho usato in questo modo un metodo per verificare il contenuto IEnumerable
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
}
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, vedi i dettagli qui https://connect.microsoft.com/VisualStudio/ feedback / dettagli / 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
Ecco una grande discussione su valutazione pigra e esecuzione differita . Fondamentalmente devi materializzare l'elenco per ottenere quel valore.
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)
L'unico modo per avere un conteggio veloce è quando la collezione originale ha un indicizzatore (come un array). Per creare un codice generico con un requisito minimo, puoi usare IEnumerable ma se hai bisogno del conteggio, allora il mio modo preferito è usare questa interfaccia:
public interface IEnumAndCount<out T> : IEnumerable<T>
{
int Count { get; }
}
Se la tua raccolta originale non ha alcun indicizzatore, l'implementazione di Count potrebbe scorrere sulla raccolta, con l'hit noto nelle prestazioni O (n).
Se non si desidera utilizzare qualcosa di simile a IEnumAndCount, la soluzione migliore è quella di utilizzare Linq.Count per i motivi forniti da Daniel Earwicker nella parte superiore di questa domanda.
Buona fortuna!
No.
Le informazioni sono disponibili ovunque nel codice che hai scritto?
Potresti sostenere che il compilatore può " vedi " che ce ne sono solo due, ma ciò significherebbe che sarebbe necessario analizzare ogni metodo iteratore cercando solo quel caso patologico specifico. E anche se lo facesse, come lo leggeresti, dati i limiti di un IEnumerable?
Suggerirei di chiamare ToList. Sì, stai eseguendo l'enumerazione in anticipo, ma hai ancora accesso al tuo elenco di elementi.
Potrebbe non offrire 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();
}
Uso IEnum < string > .ToArray < string > (). Lunghezza
e funziona bene.
Uso tale codice, se ho un elenco di stringhe:
((IList<string>)Table).Count