Считать элементы из IEnumerable <T> без итерации?

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

Допустим, я хочу повторить и написать что-то вроде обработки #n of #m.

Есть ли способ узнать значение m без итерации до моей основной итерации?

Я надеюсь, что я ясно дал понять.

sebagomez
источник

Ответы:

338

IEnumerableне поддерживает это Это по замыслу. IEnumerableиспользует ленивую оценку, чтобы получить элементы, которые вы запрашиваете, прежде чем они вам понадобятся.

Если вы хотите узнать количество элементов без перебора, которое вы можете использовать ICollection<T>, у него есть Countсвойство.

Mendelt
источник
38
Я бы предпочел ICollection над IList, если вам не нужен доступ к списку по индексатору.
Майкл Медоус
3
Я обычно просто беру Список и IList по привычке. Но особенно если вы хотите реализовать их самостоятельно, ICollection проще и имеет свойство Count. Спасибо!
Мендель
21
@Shimmy Вы повторяете и считаете элементы. Или вы вызываете Count () из пространства имен Linq, которое делает это для вас.
Мендель
1
Должно ли в обычных условиях быть достаточно просто заменить IEnumerable на IList?
Teekin
1
@Helgi Поскольку IEnumerable оценивается лениво, вы можете использовать его для вещей, которые нельзя использовать IList. Вы можете создать функцию, которая возвращает IEnumerable, который перечисляет все десятичные числа Pi, например. До тех пор, пока вы никогда не попытаетесь определить окончательный результат, это должно сработать. Вы не можете создать IList, содержащий Pi. Но это все довольно академично. Для большинства нормальных применений я полностью согласен. Если вам нужен граф, вам нужен IList. :-)
Мендель
215

Метод System.Linq.Enumerable.Countрасширения IEnumerable<T>имеет следующую реализацию:

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;

Таким образом, он пытается привести к объекту, у ICollection<T>которого есть Countсвойство, и использует его, если это возможно. В противном случае это повторяется.

Поэтому лучше всего использовать Count()метод расширения для вашего IEnumerable<T>объекта, так как вы получите максимально возможную производительность.

Дэниел Уорвикер
источник
13
Очень интересная вещь, которую он пытается бросить в ICollection<T>первую очередь.
Оскар Медерос
1
@OscarMederos Большинство методов расширения в Enumerable имеют оптимизации для последовательностей различных типов, где они будут использовать более дешевый способ, если смогут.
Шибуми
1
Упомянутое расширение доступно с .Net 3.5 и задокументировано в MSDN .
Кристиан
Я думаю , что вам не нужен общий типа Tна IEnumerator<T> enumerator = source.GetEnumerator(), просто вы можете сделать это: IEnumerator enumerator = source.GetEnumerator()и должны работать!
Jaider
7
@ Jaider - это немного сложнее, чем это. IEnumerable<T>наследует, IDisposableчто позволяет usingоператору автоматически утилизировать его. IEnumerableне. Так что, если вы позвоните в GetEnumeratorлюбом случае, вы должны закончить сvar d = e as IDisposable; if (d != null) d.Dispose();
Даниэль Earwicker
87

Просто добавив дополнительную информацию:

Count()Расширение не всегда итерации. Рассмотрим Linq to Sql, где счетчик отправляется в базу данных, но вместо возврата всех строк он выдает команду Sql Count()и возвращает этот результат.

Кроме того, компилятор (или среда выполнения) достаточно умен, чтобы вызывать Count()метод objects, если он у него есть. Так что это не так, как говорят другие респонденты, быть полностью невежественным и всегда повторять, чтобы подсчитать элементы.

Во многих случаях, когда программист просто проверяет, if( enumerable.Count != 0 )используя Any()метод расширения, if( enumerable.Any() ) это гораздо более эффективно при ленивой оценке linq, поскольку он может закорачивать, как только он может определить, есть ли какие-либо элементы. Это также более читабельно

Роберт Полсон
источник
2
Что касается коллекций и массивов. Если вы используете коллекцию, используйте .Countсвойство, поскольку оно всегда знает, какой у него размер. При запросе collection.Countнет дополнительных вычислений, он просто возвращает уже известный счетчик. То же самое, Array.lengthнасколько я знаю. Однако .Any()получает перечислитель источника с использованием using (IEnumerator<TSource> enumerator = source.GetEnumerator())и возвращает true, если это возможно enumerator.MoveNext(). Для коллекций:, if(collection.Count > 0)массивов: if(array.length > 0)и на перечислимых if(collection.Any()).
Нет
1
Первый пункт не совсем верен ... LINQ to SQL использует этот метод расширения, который отличается от этого . Если вы используете второе, то подсчет выполняется в памяти, а не как функция SQL
AlexFoxGill
1
@AlexFoxGill правильно. Если вы явно приведете свой IQueryably<T>к a, IEnumerable<T>он не выдаст счетчик sql. Когда я написал это, Linq to Sql был новым; Я думаю, что я просто настаивал на том, чтобы использовать, Any()потому что для перечислений, коллекций и sql это было намного более эффективно и читабельно (в общем). Спасибо за улучшение ответа.
Роберт Полсон
12

У моего друга есть серия постов в блоге, которые иллюстрируют, почему вы не можете этого сделать. Он создает функцию, которая возвращает IEnumerable, где каждая итерация возвращает следующее простое число, вплоть до ulong.MaxValueследующего элемента и не рассчитывается до тех пор, пока вы его не попросите. Быстрый, поп-вопрос: сколько предметов возвращено?

Вот посты, но они вроде длинные:

  1. Beyond Loops (предоставляет начальный класс EnumerableUtility, используемый в других публикациях)
  2. Приложения Iterate (начальная реализация)
  3. Сумасшедшие методы расширения: ToLazyList (оптимизация производительности)
Джоэл Коухорн
источник
2
Я действительно очень хотел бы, чтобы MS определила способ, с помощью которого перечислимые люди могли бы описать, что они могут о себе (с верным ответом «ничего не знать»). Никто из перечислимых не должен испытывать затруднений при ответе на такие вопросы, как «Знаешь ли ты себя конечным», «Знаешь ли ты себя конечным с менее чем N элементами» и «Знаешь ли ты себя бесконечным», поскольку любое перечисляемое может законно (если бесполезно) ответьте «нет» всем им. Если бы существовали стандартные способы задавать такие вопросы, для счетчиков было бы гораздо безопаснее возвращать бесконечные последовательности ...
суперкат
1
... (поскольку они могли бы сказать, что они это делают), и чтобы код предполагал, что перечислители, которые не утверждают, что возвращают бесконечные последовательности, вероятно, будут ограниченными. Обратите внимание, что включение средств для того, чтобы задавать такие вопросы (чтобы свести к минимуму шаблон, возможно, иметь свойство, возвращающее EnumerableFeaturesобъект), не требовало бы, чтобы счетчики выполняли какие-либо сложные задачи, но было бы в состоянии задать такие вопросы (наряду с некоторыми другими, такими как «Можете ли вы пообещать всегда возвращайте одну и ту же последовательность элементов »,« Можете ли вы безопасно подвергнуться воздействию кода, который не должен изменять вашу базовую коллекцию »и т. д.), было бы очень полезно
суперкат
Это было бы круто, но теперь я уверен, насколько хорошо это будет сочетаться с блоками итераторов. Вам понадобятся какие-то особые «параметры доходности» или что-то в этом роде Или, может быть, использовать атрибуты для украшения метода итератора.
Джоэл Коухорн
1
Блоки итераторов могут, при отсутствии каких-либо специальных объявлений, просто сообщать, что они ничего не знают о возвращаемой последовательности, хотя если это IEnumerator или преемник, одобренный MS (который может быть возвращен реализациями GetEnumerator, которые знали о его существовании) для поддержки дополнительной информации, C #, скорее всего, получит set yield optionsзаявление или нечто подобное для его поддержки. Если правильно спроектировать, он IEnhancedEnumeratorможет сделать такие вещи, как LINQ, намного более удобными, исключая множество «защитных» ToArrayили ToListвызовов, особенно ...
суперкат
... в тех случаях, когда подобные вещи Enumerable.Concatиспользуются для объединения большой коллекции, которая много знает о себе, с маленькой, которая этого не делает.
суперкат
10

IEnumerable не может рассчитывать без итерации.

При «нормальных» обстоятельствах классы, реализующие IEnumerable или IEnumerable <T>, такие как List <T>, могут реализовать метод Count, возвращая свойство List <T> .Count. Однако метод Count на самом деле не является методом, определенным в интерфейсе IEnumerable <T> или IEnumerable. (Фактически единственным является GetEnumerator.) И это означает, что для него не может быть обеспечена реализация для конкретного класса.

Скорее, Count это метод расширения, определенный для статического класса Enumerable. Это означает, что он может быть вызван для любого экземпляра производного класса IEnumerable <T>, независимо от реализации этого класса. Но это также означает, что он реализован в одном месте, вне любого из этих классов. Что, конечно, означает, что он должен быть реализован таким образом, который полностью независим от внутренних функций этих классов. Единственный способ подсчета - итерация.

Крис Аммерман
источник
это хорошая точка зрения, что вы не можете считать, если вы не выполните итерацию. Функциональность count связана с классами, которые реализуют IEnumerable .... таким образом, вы должны проверить, какой тип IEnumerable является входящим (проверка путем приведения), и затем вы знаете, что List <> и Dictionary <> имеют определенные способы подсчета, а затем используете те только после того, как вы знаете тип. Я нашел эту ветку очень полезной лично, так что спасибо за ваш ответ, Крис.
PositiveGuy
1
Согласно ответу Даниэля, этот ответ не является строго верным: реализация проверяет, реализует ли объект «ICollection», который имеет поле «Count». Если так, он использует это. (Было ли это , что умный в '08, я не знаю.)
ToolmakerSteve
9

В качестве альтернативы вы можете сделать следующее:

Tables.ToList<string>().Count;
Анатолий Николаев
источник
8

Нет не вообще. Одним из аспектов использования перечислимых элементов является то, что фактический набор объектов в перечислении неизвестен (заранее или даже вообще).

JesperE
источник
Важный момент, который вы затронули, заключается в том, что даже когда вы получаете этот объект IEnumerable, вы должны посмотреть, сможете ли вы привести его к типу. Это очень важный момент для тех, кто пытается использовать больше IEnumerable, как и я, в моем коде.
PositiveGuy
8

Вы можете использовать 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());
    }
}

Вы получите результат «2».

prosseek
источник
3
Это не работает для неуниверсального варианта IEnumerable (без спецификатора типа)
Marcel
Реализация .Count перечисляет все элементы, если у вас есть IEnumerable. (отличается от ICollection). ОП вопрос явно "без итерации"
JDC
5

Выходя за рамки вашего непосредственного вопроса (на который был полностью дан отрицательный ответ), если вы хотите сообщить о прогрессе при обработке перечислимого, вы можете обратиться к моему сообщению в блоге « Отчет о ходе выполнения во время запросов Linq» .

Это позволяет вам сделать это:

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

Я использовал такой способ внутри метода, чтобы проверить переданный IEnumberableконтент

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

}

Внутри такой метод:

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

}
Шахидул Хак
источник
1
Зачем это делать? «Подсчет» может быть дорогостоящим, поэтому, учитывая IEnumerable, дешевле инициализировать любые выходные данные, которые вам нужны, чтобы соответствовать значениям по умолчанию, а затем начать итерацию по «iEnum». Выберите значения по умолчанию, чтобы пустой «iEnum», который никогда не выполняет цикл, заканчивался правильным результатом. Иногда это означает добавление логического флага, чтобы узнать, был ли выполнен цикл. Конечно, это неуклюже, но полагаться на «графа» кажется неразумным. Если нужен этот флаг, код выглядит следующим образом : bool hasContents = false; if (iEnum != null) foreach (object ob in iEnum) { hasContents = true; ... your code per ob ... }.
ToolmakerSteve
... также легко добавить специальный код, который должен выполняться только в первый раз или только на итерациях, отличных от первого: `... {if (! hasContents) {hasContents = true; ... один тайм-код ..; } else {..code для всех, кроме первого раза ..} ...} "По общему признанию, это неуклюже, чем ваш простой подход, где одноразовый код будет внутри вашего if, до цикла, но если стоимость" .Count ()»может быть проблемой, то это путь.
ToolmakerSteve
3

Это зависит от того, какая версия .Net и реализация вашего объекта IEnumerable. Microsoft исправила метод IEnumerable.Count для проверки реализации и использует ICollection.Count или ICollection <TSource> .Count, подробности см. Здесь https://connect.microsoft.com/VisualStudio/feedback/details/454130.

И ниже MSIL от Ildasm для System.Core, в котором находится 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
источник
2

Результат функции IEnumerable.Count () может быть неправильным. Это очень простой пример для тестирования:

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

Результат должен быть (7,7,3,3), но фактический результат (7,7,3,17)

Роман Голубин
источник
1

Лучший способ, который я нашел, - считать, преобразовав его в список.

IEnumerable<T> enumList = ReturnFromSomeFunction();

int count = new List<T>(enumList).Count;
Абхас Бхой
источник
-1

Я бы предложил вызвать ToList. Да, вы делаете перечисление рано, но у вас все еще есть доступ к вашему списку предметов.

Джонатан Аллен
источник
-1

Это может не дать наилучшей производительности, но вы можете использовать LINQ для подсчета элементов в IEnumerable:

public int GetEnumerableCount(IEnumerable Enumerable)
{
    return (from object Item in Enumerable
            select Item).Count();
}
Хьюго
источник
Чем результат отличается от простого выполнения `` return Enumerable.Count (); "?
ToolmakerSteve
Хороший вопрос, или это действительно ответ на этот вопрос Stackoverflow.
Уго
-1

Я думаю, что самый простой способ сделать это

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

Ссылка: system.linq.enumerable

Соввик Рой
источник
Вопрос в том, "считать элементы из IEnumerable <T> без итерации?" Как это отвечает на это?
Загадка
-3

Я пользуюсь IEnum<string>.ToArray<string>().Lengthи все отлично работает.

Оливер Кёттер
источник
Это должно работать нормально. IEnumerator <Object> .ToArray <Object> .Length
din
1
Почему вы делаете это, а не более сжатое и более быстрое решение, уже приведенное в ответе Даниэля, написанном на три года раньше, чем у вас , " IEnum<string>.Count();"?
ToolmakerSteve
Ты прав. Каким-то образом я пропустил ответ Даниэля, возможно, потому что он цитировал из реализации, и я подумал, что он реализовал метод расширения, и я искал решение с меньшим количеством кода.
Оливер
Каков наиболее приемлемый способ обработки моего плохого ответа? Должен ли я удалить это?
Оливер
-3

Я использую такой код, если у меня есть список строк:

((IList<string>)Table).Count
Я голодный
источник