web-dev-qa-db-fra.com

Compter les éléments d'un IEnumerable <T> sans itérer?

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

Disons que je veux itérer sur ceux-ci et écrire quelque chose comme traitement #n de #m.

Existe-t-il un moyen de déterminer la valeur de m sans itérer avant mon itération principale?

J'espère que j'ai été clair.

298
sebagomez

IEnumerable ne supporte pas cela. C'est par conception. IEnumerable utilise une évaluation paresseuse pour obtenir les éléments que vous demandez juste avant de les utiliser.

Si vous voulez connaître le nombre d'éléments sans les parcourir, vous pouvez utiliser ICollection<T>, il possède une propriété Count.

314
Mendelt

La méthode d'extension System.Linq.Enumerable.Count sur IEnumerable<T> a l'implémentation suivante:

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;

Il essaie donc de transtyper en ICollection<T>, qui a une propriété Count, et l’utilise si possible. Sinon, il répète.

Donc, votre meilleur choix est d'utiliser la méthode d'extension Count() sur votre objet IEnumerable<T>, car vous obtiendrez ainsi les meilleures performances possibles.

204
Daniel Earwicker

Juste en ajoutant des infos supplémentaires:

L'extension Count() n'itère pas toujours. Considérez Linq en Sql, où le compte va à la base de données, mais au lieu de ramener toutes les lignes, il lance la commande Sql Count() et renvoie le résultat.

De plus, le compilateur (ou runtime) est suffisamment intelligent pour appeler la méthode objects Count() si elle en a une. Donc, c’est pas comme d’autres répondeurs, complètement ignorants et toujours itératifs pour compter les éléments.

Dans de nombreux cas où le programmeur vérifie simplement if( enumerable.Count != 0 ) à l'aide de la méthode d'extension Any(), comme dans if( enumerable.Any() ) est beaucoup plus efficace avec l'évaluation paresseuse de linq car il peut court-circuiter une fois qu'il peut le déterminer sont des éléments. C'est aussi plus lisible

86
Robert Paulson

Un de mes amis a une série de billets de blog qui illustrent pourquoi vous ne pouvez pas faire cela. Il crée une fonction qui retourne un IEnumerable où chaque itération renvoie le nombre premier suivant, jusqu'à ulong.MaxValue, et l'élément suivant n'est pas calculé tant que vous ne le demandez pas. Rapide, question pop: combien d'articles sont retournés?

Voici les messages, mais ils sont plutôt longs:

  1. Beyond Loops (fournit une classe EnumerableUtility initiale utilisée dans les autres messages)
  2. Applications d'Iterate (Mise en oeuvre initiale)
  3. Méthodes d'extensions folles: ToLazyList (Optimisations de performances)
12
Joel Coehoorn

IEnumerable ne peut pas compter sans itérer.

Dans des circonstances "normales", il serait possible aux classes implémentant IEnumerable ou IEnumerable <T>, telles que List <T>, d'implémenter la méthode Count en renvoyant la propriété List <T> .Count. Toutefois, la méthode Count n'est pas réellement une méthode définie sur l'interface IEnumerable <T> ou IEnumerable. (Le seul qui soit, en fait, est GetEnumerator.) Cela signifie qu'une implémentation spécifique à une classe ne peut pas être fournie.

Il s’agit plutôt d’une méthode d’extension, définie dans la classe statique Enumerable. Cela signifie qu'il peut être appelé sur n'importe quelle instance d'une classe dérivée IEnumerable <T>, quelle que soit l'implémentation de cette classe. Mais cela signifie également qu'il est implémenté dans un seul endroit, extérieur à l'une de ces classes. Ce qui bien sûr signifie qu'il doit être implémenté de manière totalement indépendante des internes de ces classes. Le seul moyen de compter est par itération.

10
Chris Ammerman

Sinon, vous pouvez faire ce qui suit:

Tables.ToList<string>().Count;
9
h_alsharaf

Non, pas en général. L'utilisation d'énumérables a notamment pour avantage que l'ensemble des objets de l'énumération n'est pas connu (à l'avance, voire pas du tout).

8
JesperE

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

Vous obtiendrez le résultat '2'.

8
prosseek

Au-delà de votre question immédiate (à laquelle une réponse complète a été apportée par la négative), si vous souhaitez signaler les progrès réalisés lors du traitement d'un énumérable, vous pouvez consulter mon article de blog Rapport d'avancement pendant les requêtes Linq .

Cela vous permet de faire ceci:

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

J'ai utilisé cette méthode dans une méthode pour vérifier le contenu de IEnumberable passé

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

}

Dans une méthode comme celle-ci:

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

}
4
Shahidul Haque

Cela dépend de la version de .Net et de la mise en oeuvre de votre objet IEnumerable. Microsoft a corrigé la méthode IEnumerable.Count pour vérifier la mise en œuvre et utilise ICollection.Count ou ICollection <TSource> .Count, voir les détails ici https://connect.Microsoft.com/VisualStudio/feedback/details/4541

Et ci-dessous se trouve le MSIL de Ildasm pour System.Core, dans lequel réside le 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
3
prabug

Le résultat de la fonction IEnumerable.Count () peut être incorrect. Voici un exemple très simple à tester:

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

Le résultat doit être (7,7,3,3) mais le résultat réel est (7,7,3,17)

2
Roman Golubin

Voici une excellente discussion sur évaluation paresseuse et exécution différée . Fondamentalement, vous devez matérialiser la liste pour obtenir cette valeur.

2
JP Alioto

Le seul moyen d'avoir un décompte rapide est lorsque la collection d'origine comporte un indexeur (comme un tableau). Afin de créer du code générique avec une exigence minimale, vous pouvez utiliser IEnumerable, mais si vous avez également besoin du nombre, ma méthode préférée consiste à utiliser cette interface:


    public interface IEnumAndCount<out T> : IEnumerable<T>
    {
        int Count { get; }
    }

Si votre collection d'origine ne possède pas d'indexeur, votre implémentation Count peut itérer sur la collection, avec le hit connu dans la performance O (n).

Si vous ne souhaitez pas utiliser quelque chose de similaire à IEnumAndCount, votre meilleur choix est d'aller chez Linq.Count pour les raisons données par Daniel Earwicker vers le haut de cette question.

Bonne chance !

1
Eric Ouellet

Je suggère d'appeler ToList. Oui, vous faites l'énumération tôt, mais vous avez toujours accès à votre liste d'éléments.

0
Jonathan Allen

Non.

Voyez-vous cette information disponible n'importe où dans le code que vous avez écrit?

Vous pourriez faire valoir que le compilateur peut "voir" qu'il n'y en a que deux, mais cela voudrait dire qu'il devrait analyser chaque méthode d'itérateur en recherchant uniquement ce cas pathologique spécifique. Et même si c'était le cas, comment le liriez-vous, étant donné les limites d'un IEnumerable?

0
James Curran

Les performances ne sont peut-être pas optimales, mais vous pouvez utiliser LINQ pour compter les éléments dans un IEnumerable:

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