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

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

Disons que je veux les itérer et écrire quelque chose comme le traitement #n de #m.

Existe-t-il un moyen de connaître la valeur de m sans itérer avant mon itération principale?

J'espère que je me suis bien fait comprendre.

sebagomez
la source

Réponses:

338

IEnumerablene prend pas en charge cela. C'est par conception. IEnumerableutilise une évaluation paresseuse pour obtenir les éléments que vous demandez juste avant d'en avoir besoin.

Si vous voulez connaître le nombre d'éléments sans les répéter, vous pouvez les utiliser ICollection<T>, il a une Countpropriété.

Mendelt
la source
38
Je préférerais ICollection à IList si vous n'avez pas besoin d'accéder à la liste par indexeur.
Michael Meadows
3
Je prends habituellement List et IList par habitude. Mais surtout si vous voulez les implémenter vous-même, ICollection est plus facile et possède également la propriété Count. Merci!
Mendelt
21
@Shimmy Vous parcourez et comptez les éléments. Ou vous appelez Count () à partir de l'espace de noms Linq qui le fait pour vous.
Mendelt
1
Le simple remplacement de IEnumerable par IList devrait-il être suffisant dans des circonstances normales?
Teekin
1
@Helgi Parce que IEnumerable est évalué paresseusement, vous pouvez l'utiliser pour des choses que IList ne peut pas être utilisé. Vous pouvez construire une fonction qui retourne un IEnumerable qui énumère toutes les décimales de Pi par exemple. Tant que vous n'essayez jamais de chercher le résultat complet, cela devrait fonctionner. Vous ne pouvez pas créer une IList contenant Pi. Mais c'est assez académique. Pour la plupart des utilisations normales, je suis entièrement d'accord. Si vous avez besoin de Count, vous avez besoin d'IList. :-)
Mendelt
215

La System.Linq.Enumerable.Countméthode d'extension on 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 convertir en ICollection<T>, qui a une Countpropriété, et l'utilise si possible. Sinon, il itère.

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

Daniel Earwicker
la source
13
Très intéressant la chose qu'il essaie de lancer en ICollection<T>premier.
Oscar Mederos
1
@OscarMederos La plupart des méthodes d'extension dans Enumerable ont des optimisations pour les séquences des différents types où elles utiliseront la manière la moins chère si elles le peuvent.
Shibumi
1
L'extension mentionnée est disponible depuis .Net 3.5 et documentée dans MSDN .
Christian
Je pense que vous n'avez pas besoin du type générique Tsur IEnumerator<T> enumerator = source.GetEnumerator(), simplement vous pouvez le faire: IEnumerator enumerator = source.GetEnumerator()et devrait fonctionner!
Jaider
7
@Jaider - c'est un peu plus compliqué que ça. IEnumerable<T>hérite, IDisposablece qui permet à l' usinginstruction de le supprimer automatiquement. IEnumerablene fait pas. Donc, si vous appelez dans les GetEnumeratordeux sens, vous devriez terminer avecvar d = e as IDisposable; if (d != null) d.Dispose();
Daniel Earwicker
87

Il suffit d'ajouter quelques informations supplémentaires:

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

De plus, le compilateur (ou runtime) est suffisamment intelligent pour appeler la Count()méthode objets s'il en a une. Ce n'est donc pas comme d'autres intervenants le disent, étant complètement ignorant et itérant toujours pour compter les éléments.

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

Robert Paulson
la source
2
En ce qui concerne les collections et les tableaux. Si vous utilisez une collection, utilisez la .Countpropriété car elle sait toujours que c'est sa taille. Lors d'une requête, collection.Countil n'y a pas de calcul supplémentaire, il renvoie simplement le nombre déjà connu. Pareil pour Array.lengthautant que je sache. Cependant, .Any()obtient l'énumérateur de la source à l'aide de using (IEnumerator<TSource> enumerator = source.GetEnumerator())et renvoie true s'il le peut enumerator.MoveNext(). Pour les collections if(collection.Count > 0):, les tableaux: if(array.length > 0)et sur les énumérateurs le font if(collection.Any()).
Non
1
Le premier point n'est pas strictement vrai ... LINQ to SQL utilise cette méthode d'extension qui n'est pas la même que celle-ci . Si vous utilisez la seconde, le comptage est effectué en mémoire, pas comme une fonction SQL
AlexFoxGill
1
@AlexFoxGill est correct. Si vous convertissez explicitement votre IQueryably<T>en un, IEnumerable<T>il n'émettra pas de compte sql. Quand j'ai écrit cela, Linq to Sql était nouveau; Je pense que je poussais juste à utiliser Any()car pour les énumérables, les collections et sql c'était beaucoup plus efficace et lisible (en général). Merci d'avoir amélioré la réponse.
Robert Paulson
12

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 retourne le nombre premier suivant, jusqu'à ulong.MaxValue, et l'élément suivant n'est pas calculé jusqu'à ce que vous le demandiez. Question rapide et pop: combien d'articles sont retournés?

Voici les articles, mais ils sont assez longs:

  1. Beyond Loops (fournit une première classe EnumerableUtility utilisée dans les autres articles)
  2. Applications d'itération (mise en œuvre initiale)
  3. Méthodes d'extension folles: ToLazyList (optimisations des performances)
Joel Coehoorn
la source
2
Je souhaite vraiment vraiment que MS ait défini un moyen de demander aux énumérables de décrire ce qu'ils peuvent faire d'eux-mêmes ("ne rien savoir" étant une réponse valide). Aucun énumérable ne devrait avoir de difficulté à répondre à des questions telles que "vous savez-vous être fini", "vous savez-vous être fini avec moins de N éléments" et "vous savez-vous être infini", car tout énumérable pourrait légitimement (si inutile) répondez «non» à tous. S'il existait un moyen standard de poser de telles questions, il serait beaucoup plus sûr pour les enquêteurs de renvoyer des séquences sans fin ...
supercat
1
... (car ils pourraient dire qu'ils le font), et pour que le code suppose que les énumérateurs qui ne prétendent pas renvoyer des séquences sans fin sont susceptibles d'être limités. Notez que l'inclusion d'un moyen de poser de telles questions (pour minimiser le passe-partout, probablement qu'une propriété retourne un EnumerableFeaturesobjet) n'obligerait pas les enquêteurs à faire quelque chose de difficile, mais être capable de poser de telles questions (ainsi que d'autres comme «Pouvez-vous promettre de retourner toujours la même séquence d'éléments "," Pouvez-vous être exposé en toute sécurité à du code qui ne devrait pas altérer votre collection sous-jacente ", etc.) serait très utile.
supercat
Ce serait assez cool, mais je suis maintenant sûr de la façon dont cela s'intégrerait aux blocs d'itérateur. Vous auriez besoin d'une sorte «d'options de rendement» spéciales ou quelque chose du genre. Ou utilisez peut-être des attributs pour décorer la méthode de l'itérateur.
Joel Coehoorn
1
Les blocs d'itérateur pourraient, en l'absence de toute autre déclaration spéciale, simplement signaler qu'ils ne savent rien de la séquence retournée, bien que si IEnumerator ou un successeur approuvé par MS (qui pourrait être retourné par les implémentations GetEnumerator qui connaissaient son existence) devaient prendre en charge des informations supplémentaires, C # obtiendrait probablement une set yield optionsdéclaration ou quelque chose de similaire pour la soutenir. S'il est bien conçu, un IEnhancedEnumeratorpourrait rendre des choses comme LINQ beaucoup plus utilisables en éliminant beaucoup de "défensifs" ToArrayou d' ToListappels, en particulier ...
supercat
... dans les cas où des choses comme Enumerable.Concat sont utilisées pour combiner une grande collection qui en sait beaucoup sur elle-même avec une petite qui ne le sait pas.
supercat
10

IEnumerable ne peut pas compter sans itérer.

Dans des circonstances "normales", il serait possible pour les 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 est, en fait, est GetEnumerator.) Et cela signifie qu'une implémentation spécifique à la classe ne peut pas être fournie pour cela.

Au contraire, Count c'est une méthode d'extension, définie sur 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é en un seul endroit, externe à l'une de ces classes. Ce qui bien sûr signifie qu'il doit être implémenté d'une manière complètement indépendante des internes de ces classes. La seule façon de faire le comptage est via l'itération.

Chris Ammerman
la source
c'est un bon point de ne pouvoir compter que si vous répétez. La fonctionnalité de comptage est liée aux classes qui implémentent IEnumerable .... vous devez donc vérifier quel type IEnumerable est entrant (vérifier par transtypage) et puis vous savez que List <> et Dictionary <> ont certaines façons de compter puis d'utiliser ceux-là seulement après avoir connu le type. J'ai trouvé ce fil très utile personnellement, merci aussi pour votre réponse Chris.
PositiveGuy
1
Selon la réponse de Daniel, cette réponse n'est pas strictement vraie: l'implémentation vérifie si l'objet implémente "ICollection", qui a un champ "Count". Si c'est le cas, il l'utilise. (Que ce soit si intelligent en '08, je ne sais pas.)
ToolmakerSteve
9

Vous pouvez également effectuer les opérations suivantes:

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

Non, pas en général. Un point dans l'utilisation des énumérables est que l'ensemble réel d'objets dans l'énumération n'est pas connu (à l'avance, ou même pas du tout).

JesperE
la source
Le point important que vous avez soulevé est que même lorsque vous obtenez cet objet IEnumerable, vous devez voir si vous pouvez le convertir pour déterminer de quel type il s'agit. C'est un point très important pour ceux qui essaient d'utiliser plus d'IEnumerable comme moi dans mon code.
PositiveGuy
8

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».

prosseek
la source
3
Cela ne fonctionne pas pour la variante non générique IEnumerable (sans spécificateur de type)
Marcel
L'implémentation de .Count énumère tous les éléments si vous avez un IEnumerable. (différent pour ICollection). La question du PO est clairement "sans itération"
JDC
5

Au-delà de votre question immédiate (à laquelle une réponse négative complète a été donnée), si vous cherchez à signaler les progrès tout en traitant un énumérable, vous voudrez peut-être consulter mon article de blog Rapporter les progrès pendant les requêtes Linq .

Il 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
      };
Samuel Jack
la source
4

J'ai utilisé cette méthode à l'intérieur d'une méthode pour vérifier le IEnumberablecontenu transmis

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

}

À l'intérieur d'une méthode comme celle-ci:

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

}
Shahidul Haque
la source
1
Pourquoi faire ça? "Count" peut être coûteux, donc étant donné un IEnumerable, il est moins cher d'initialiser toutes les sorties dont vous avez besoin pour les valeurs par défaut appropriées, puis de commencer l'itération sur "iEnum". Choisissez des valeurs par défaut telles qu'un "iEnum" vide, qui n'exécute jamais la boucle, se retrouve avec un résultat valide. Parfois, cela signifie ajouter un drapeau booléen pour savoir si la boucle a été exécutée. Certes, c'est maladroit, mais s'appuyer sur "Count" semble imprudent. Si besoin de ce drapeau, regards de code comme: bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }.
ToolmakerSteve
... il est également facile d'ajouter du code spécial qui ne doit être effectué que la première fois, ou uniquement sur des itérations autres que la première fois: `... {if (! hasContents) {hasContents = true; ..un code temporel ..; } else {..code pour tous sauf la première fois ..} ...} "Certes, c'est plus maladroit que votre approche simple, où le code à usage unique serait à l'intérieur de votre if, avant la boucle, mais si le coût de" .Count () "pourrait être un problème, alors c'est la voie à suivre.
ToolmakerSteve
3

Cela dépend de la version de .Net et de l'implémentation de votre objet IEnumerable. Microsoft a corrigé la méthode IEnumerable.Count pour vérifier l'implémentation et utilise ICollection.Count ou ICollection <TSource> .Count, voir les détails ici https://connect.microsoft.com/VisualStudio/feedback/details/454130

Et ci-dessous se trouve le MSIL d'Ildasm pour System.Core, dans lequel réside 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
la source
2

Le résultat de la fonction IEnumerable.Count () peut être incorrect. Voici un échantillon 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)

Roman Golubin
la source
1

La meilleure façon que j'ai trouvée est de compter en la convertissant en liste.

IEnumerable<T> enumList = ReturnFromSomeFunction();

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

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

Jonathan Allen
la source
-1

Il ne peut pas donner les meilleures performances, 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();
}
Hugo
la source
En quoi le résultat diffère-t-il de simplement faire "` return Enumerable.Count (); "?
ToolmakerSteve
Bonne question, ou est-ce en fait une réponse à cette question Stackoverflow.
Hugo
-1

Je pense que la façon la plus simple de le faire

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

Référence: system.linq.enumerable

Sowvik Roy
la source
La question est "Compter les éléments d'un IEnumerable <T> sans itérer?" Comment cela répond-il à cela?
Énigmativité
-3

J'utilise IEnum<string>.ToArray<string>().Lengthet ça marche bien.

Oliver Kötter
la source
Cela devrait bien fonctionner. IEnumerator <Object> .ToArray <Object> .Length
din
1
Pourquoi faites-vous cela, plutôt que la solution plus concise et plus rapide déjà donnée dans la réponse très appréciée de Daniel écrite trois ans plus tôt que la vôtre , " IEnum<string>.Count();"?
ToolmakerSteve
Vous avez raison. D'une certaine manière, j'ai ignoré la réponse de Daniel, peut-être parce qu'il a cité l'implémentation et j'ai pensé qu'il implémentait une méthode d'extension et je cherchais une solution avec moins de code.
Oliver Kötter
Quelle est la manière la plus acceptée de traiter ma mauvaise réponse? Dois-je le supprimer?
Oliver Kötter
-3

J'utilise un tel code, si j'ai une liste de chaînes:

((IList<string>)Table).Count
Moi faim
la source