Policzyć elementy z IEnumerable <T> bez iteracji?


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

Powiedzmy, że chcę je powtórzyć i napisz coś takiego jak przetwarzanie #n z #m.

Czy istnieje sposób, aby dowiedzieć się o wartości m bez iteracji przed moją główną iteracją?

Mam nadzieję, że wyraziłem się jasno.

Odpowiedzi:


338

IEnumerablenie obsługuje tego. To jest z założenia. IEnumerableużywa leniwej oceny, aby uzyskać elementy, o które prosisz tuż przed ich użyciem.

Jeśli chcesz poznać liczbę elementów bez iteracji nad nimi, możesz użyć ICollection<T>tej Countwłaściwości.


38
Wolę ICollection niż IList, jeśli nie potrzebujesz dostępu do listy przez indeksatora.
Michael Meadows

3
Zwykle chwytam List i IList z przyzwyczajenia. Ale szczególnie, jeśli chcesz je zaimplementować samodzielnie, ICollection jest łatwiejszy i ma również właściwość Count. Dzięki!
Mendelt,

21
@Shimmy Wykonujesz iterację i liczysz elementy. Lub wywołujesz Count () z przestrzeni nazw Linq, która robi to za Ciebie.
Mendelt,

1
Czy zwykłe zastąpienie IEnumerable IList powinno wystarczyć w normalnych okolicznościach?
Teekin,

1
@Helgi Ponieważ IEnumerable jest oceniany leniwie, możesz go używać do rzeczy, których nie można użyć IList. Możesz zbudować funkcję, która zwraca IEnumerable, która wylicza na przykład wszystkie miejsca dziesiętne liczby Pi. Tak długo, jak nigdy nie próbujesz przepowiadać pełnego wyniku, powinno działać. Nie można utworzyć IList zawierającej Pi. Ale to wszystko dość akademickie. W przypadku większości normalnych zastosowań całkowicie się zgadzam. Jeśli potrzebujesz hrabiego, potrzebujesz IList. :-)
Mendelt,

215

Metoda System.Linq.Enumerable.Countrozszerzenia IEnumerable<T>ma następującą implementację:

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;

Próbuje więc przesyłać do ICollection<T>, który ma Countwłaściwość, i używa go, jeśli to możliwe. W przeciwnym razie iteruje.

Więc najlepszym rozwiązaniem jest użycie Count()metody rozszerzenia na IEnumerable<T>obiekcie, ponieważ uzyskasz w ten sposób najlepszą możliwą wydajność.


13
Bardzo interesująca rzecz, na którą próbuje rzucić ICollection<T>najpierw.
Oscar Mederos,

1
@OscarMederos Większość metod rozszerzenia w Enumerable ma optymalizacje dla sekwencji różnych typów, w których będą używać tańszego sposobu, jeśli to możliwe.
Shibumi,

1
Wspomniane rozszerzenie jest dostępne od .Net 3.5 i udokumentowane w MSDN .
Christian

Myślę, że nie potrzebujesz Twłączonego rodzaju ogólnego IEnumerator<T> enumerator = source.GetEnumerator(), po prostu możesz to zrobić: IEnumerator enumerator = source.GetEnumerator()i powinien działać!
Jaider

7
@Jaider - jest to nieco bardziej skomplikowane. IEnumerable<T>dziedziczy, IDisposableco pozwala usinginstrukcji na automatyczne usunięcie. IEnumerablenie. Więc jeśli zadzwonisz w GetEnumeratorktórąkolwiek stronę, powinieneś skończyć zvar d = e as IDisposable; if (d != null) d.Dispose();
Daniel Earwicker

87

Dodając dodatkowe informacje:

Count()Rozbudowa nie zawsze iteracji. Rozważ Linq do Sql, gdzie liczba trafia do bazy danych, ale zamiast przywracać wszystkie wiersze, wydaje Count()polecenie Sql i zwraca ten wynik.

Ponadto kompilator (lub środowisko wykonawcze) jest na tyle inteligentny, że wywoła Count()metodę object , jeśli ją posiada. Więc nie jest tak, jak mówią inni respondenci, będąc całkowicie ignorantem i zawsze iterując, aby policzyć elementy.

W wielu przypadkach, gdy programista sprawdza po prostu if( enumerable.Count != 0 )za pomocą Any()metody rozszerzenia, ponieważ if( enumerable.Any() ) jest to o wiele bardziej wydajne z leniwą oceną linq, ponieważ może zwierać, gdy może stwierdzić, że są jakieś elementy. Jest także bardziej czytelny


2
W odniesieniu do kolekcji i tablic. Jeśli zdarzy ci się użyć kolekcji, użyj .Countwłaściwości, ponieważ zawsze wie, że ma rozmiar. Podczas zapytania collection.Countnie ma dodatkowych obliczeń, po prostu zwraca on już znaną liczbę. To samo o Array.lengthile wiem. Jednak .Any()pobiera moduł wyliczający źródła using (IEnumerator<TSource> enumerator = source.GetEnumerator())i zwraca true, jeśli to możliwe enumerator.MoveNext(). W przypadku kolekcji if(collection.Count > 0):, tablice: if(array.length > 0)i na wyliczeniach if(collection.Any()).
Nie

1
Pierwszy punkt nie jest do końca prawdziwy ... LINQ to SQL używa tej metody rozszerzenia, która nie jest taka sama jak ta . Jeśli użyjesz drugiego, liczenie jest wykonywane w pamięci, a nie jako funkcja SQL
AlexFoxGill

1
@AlexFoxGill jest poprawny. Jeśli jednoznacznie rzucisz IQueryably<T>na IEnumerable<T>nie, nie będzie wystawiać liczby sql. Kiedy to napisałem, Linq do Sql był nowy; Myślę, że po prostu naciskałem na użycie, Any()ponieważ dla wyliczeń, kolekcji i sql było znacznie bardziej wydajne i czytelne (ogólnie). Dzięki za poprawę odpowiedzi.
Robert Paulson,

12

Mój przyjaciel ma serię postów na blogu, które ilustrują, dlaczego nie możesz tego zrobić. Tworzy funkcję, która zwraca IEnumerable, gdzie każda iteracja zwraca następną liczbę pierwszą, aż do ulong.MaxValue, a następny element nie jest obliczany, dopóki go nie poprosisz. Szybkie, popowe pytanie: ile przedmiotów jest zwracanych?

Oto posty, ale są one dość długie:

  1. Beyond Loops (zapewnia początkową klasę EnumerableUtility używaną w innych postach)
  2. Zastosowania Iterate (wstępne wdrożenie)
  3. Szalone metody rozszerzenia: ToLazyList (optymalizacje wydajności)

2
Naprawdę naprawdę chciałbym, żeby stwardnienie rozsiane zdefiniowało sposób, by poprosić wyliczenia, by opisali, co mogą o sobie (z „nie wiedząc nic” jest prawidłową odpowiedzią). Żaden wyliczenie nie powinno mieć trudności z odpowiedzią na pytania takie jak „Czy wiesz, że jesteś skończony”, „Czy wiesz, że jesteś skończony z mniej niż N elementów”, i „Czy wiesz, że jesteś nieskończony”, ponieważ każdy wyliczalny mógłby zgodnie z prawem (jeśli nieprzydatnie) odpowiedz wszystkim „nie”. Gdyby istniały standardowe sposoby zadawania takich pytań, byłoby znacznie bezpieczniej dla wyliczających zwracanie niekończących się sekwencji ...
supercat

1
... (ponieważ mogliby powiedzieć, że tak robią), a kod zakłada, że ​​jednostki wyliczające, które nie twierdzą, że zwracają niekończące się sekwencje, prawdopodobnie zostaną ograniczone. Zauważ, że uwzględnienie sposobu zadawania takich pytań (aby zminimalizować płytkę kotłową, prawdopodobnie właściwość zwróci EnumerableFeaturesobiekt) nie wymagałoby od liczących robienia czegokolwiek trudnego, ale możliwość zadawania takich pytań (wraz z innymi, takimi jak „Czy możesz obiecać zawsze zwracaj tę samą sekwencję elementów ”,„ Czy możesz być narażony na kod, który nie powinien zmieniać twojej podstawowej kolekcji ”itp.) byłby bardzo przydatny.
supercat

To byłoby całkiem fajne, ale teraz jestem pewien, jak dobrze to by się połączyło z blokami iteratora. Będziesz potrzebować jakiegoś specjalnego „opcji dochodu” lub czegoś takiego. A może użyj atrybutów, aby udekorować metodę iteratora.
Joel Coehoorn

1
Bloki iteratora mogłyby, przy braku jakichkolwiek innych specjalnych deklaracji, po prostu zgłosić, że nie wiedzą nic o zwróconej sekwencji, chociaż jeśli IEnumerator lub następca zatwierdzony przez MS (który może zostać zwrócony przez implementacje GetEnumerator, które wiedziały o jego istnieniu) byłyby obsługiwać dodatkowe informacje, C # prawdopodobnie otrzyma set yield optionsinstrukcję lub coś podobnego do jej obsługi. Jeśli jest odpowiednio zaprojektowany, IEnhancedEnumeratormoże sprawić, że LINQ będzie znacznie bardziej użyteczny, eliminując wiele „defensywnych” ToArraylub ToListpołączeń, szczególnie ...
supercat

... w przypadkach, gdy takie rzeczy Enumerable.Concatsą używane do łączenia dużej kolekcji, która dużo o sobie wie, z małą, która nie wie.
supercat

10

IEnumerable nie może się liczyć bez iteracji.

W „normalnych” okolicznościach klasy implementujące IEnumerable lub IEnumerable <T>, takie jak List <T>, mogłyby zaimplementować metodę Count, zwracając właściwość List <T> .Count. Jednak metoda Count nie jest w rzeczywistości metodą zdefiniowaną w interfejsie IEnumerable <T> lub IEnumerable. (Jedynym, który w rzeczywistości jest GetEnumerator). A to oznacza, że ​​nie można zapewnić dla niego implementacji specyficznej dla klasy.

Raczej licz, to metoda rozszerzenia zdefiniowana w klasie statycznej Enumerable. Oznacza to, że można go wywołać w dowolnej instancji klasy pochodnej <T> IEnumerable, niezależnie od implementacji tej klasy. Ale oznacza to również, że jest implementowany w jednym miejscu, zewnętrznym względem którejkolwiek z tych klas. Co oczywiście oznacza, że ​​musi zostać zaimplementowany w sposób całkowicie niezależny od wewnętrznych elementów tej klasy. Jedynym takim sposobem liczenia jest iteracja.


to dobra rzecz, że nie można liczyć, dopóki się nie powtórzy. Funkcja liczenia jest powiązana z klasami, które implementują IEnumerable .... dlatego musisz sprawdzić, jaki typ przychodzi IEnumerable (sprawdź przez rzutowanie), a następnie wiesz, że List <> i Dictionary <> mają pewne sposoby na policzenie, a następnie użycie te dopiero, gdy poznasz typ. Uważam, że ten wątek jest bardzo przydatny osobiście, więc dziękuję również za odpowiedź Chris.
PositiveGuy,

1
Zgodnie z odpowiedzią Daniela odpowiedź ta nie jest do końca prawdą: implementacja NIE sprawdza, czy obiekt implementuje „ICollection”, który ma pole „Count”. Jeśli tak, używa go. (Nie wiem, czy to było takie mądre w '08).
ToolmakerSteve


8

Nie, ogólnie nie. Jednym z punktów przy użyciu wyliczeń jest to, że rzeczywisty zestaw obiektów w wyliczeniu nie jest znany (wcześniej, a nawet wcale).


Ważną kwestią, o której wspomniałeś, jest to, że nawet gdy otrzymasz ten obiekt IEnumerable, musisz sprawdzić, czy możesz go rzutować, aby dowiedzieć się, jaki to typ. To bardzo ważna kwestia dla tych, którzy próbują używać w moim kodzie więcej IENumerable, takich jak ja.
PositiveGuy,

8

Możesz użyć 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());
    }
}

Otrzymasz wynik „2”.


3
Nie działa to w przypadku wariantu generycznego IEnumerable (bez specyfikatora typu)
Marcel

Implementacja .Count wylicza wszystkie elementy, jeśli masz IEnumerable. (różne dla ICollection). Pytanie OP jest jednoznacznie „bez iteracji”
JDC

5

Wykraczając poza twoje bezpośrednie pytanie (na które dokładnie udzielono odpowiedzi przeczącej), jeśli chcesz zgłosić postęp podczas przetwarzania wyliczenia, możesz spojrzeć na mój post na blogu Zgłaszanie postępów podczas zapytań Linq .

Pozwala ci to zrobić:

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

4

Kiedyś taki sposób wewnątrz metody, by sprawdzić przekazany w IEnumberabletreści

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

}

Wewnątrz metody takiej jak ta:

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

}

1
Dlaczego to robisz „Liczenie” może być kosztowne, więc biorąc pod uwagę IEnumerable, tańsze jest inicjowanie wszelkich potrzebnych danych wyjściowych do odpowiednich wartości domyślnych, a następnie rozpoczęcie iteracji po „iEnum”. Wybierz wartości domyślne, tak aby puste „iEnum”, które nigdy nie wykonuje pętli, kończy się prawidłowym wynikiem. Czasami oznacza to dodanie flagi logicznej, aby wiedzieć, czy pętla została wykonana. To prawda, że ​​jest to niezdarne, ale poleganie na „Count” wydaje się niemądre. Jeśli potrzebujesz tej flagi, wygląda jak kod: bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }.
ToolmakerSteve

... łatwo jest również dodać specjalny kod, który powinien być wykonany tylko za pierwszym razem lub tylko w iteracjach innych niż za pierwszym razem: `... {if (! hasContents) {hasContents = true; .. jeden kod czasu ..; } else {... kod dla wszystkich oprócz pierwszego ..} ...} "To prawda, że ​​jest to bardziej niezręczne niż twoje proste podejście, w którym jednorazowy kod byłby wewnątrz twojego if, przed pętlą, ale jeśli koszt" .Count () ”może budzić obawy, to jest właściwy sposób.
ToolmakerSteve

3

To zależy od wersji .Net i implementacji Twojego obiektu IEnumerable. Microsoft naprawił metodę IEnumerable.Count w celu sprawdzenia implementacji i używa ICollection.Count lub ICollection <TSource> .Count, zobacz szczegóły tutaj https://connect.microsoft.com/VisualStudio/feedback/details/454130

A poniżej znajduje się MSIL z Ildasm dla System.Core, w którym rezyduje 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


2

Wynik funkcji IEnumerable.Count () może być nieprawidłowy. To jest bardzo prosta próbka do przetestowania:

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

Wynik musi wynosić (7,7,3,3), ale rzeczywisty wynik to (7,7,3,17)


1

Najlepszym sposobem, jaki znalazłem, jest przeliczenie na listę.

IEnumerable<T> enumList = ReturnFromSomeFunction();

int count = new List<T>(enumList).Count;

-1

Sugerowałbym wywołanie ToList. Tak, wyliczasz wcześniej, ale nadal masz dostęp do swojej listy elementów.


-1

Może nie dawać najlepszej wydajności, ale możesz użyć LINQ do zliczenia elementów w IEnumerable:

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

Czym różni się wynik od wykonania po prostu „return return Enumerable.Count ();”?
ToolmakerSteve

Dobre pytanie, czy jest to właściwie odpowiedź na to pytanie Stackoverflow.
Hugo,


-3

Używam IEnum<string>.ToArray<string>().Lengthi działa dobrze.


To powinno działać dobrze. IEnumerator <Object> .ToArray <Object> .Length
din

1
Dlaczego to robisz, a nie bardziej zwięzłe i szybsze rozwiązanie podane już w wysoko ocenianej odpowiedzi Daniela napisanej trzy lata wcześniej niżIEnum<string>.Count();”?
ToolmakerSteve

Masz rację. Jakoś przeoczyłem odpowiedź Daniela, być może dlatego, że cytował z implementacji i myślałem, że zaimplementował metodę rozszerzenia i szukałem rozwiązania z mniejszym kodem.
Oliver Kötter

Jaki jest najbardziej akceptowany sposób radzenia sobie z moją złą odpowiedzią? Czy powinienem to usunąć?
Oliver Kötter

-3

Używam takiego kodu, jeśli mam listę ciągów:

((IList<string>)Table).Count
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.