Порахуйте елементи з IEnumerable <T> без повторення?


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

Скажімо, я хочу повторити їх і написати щось на зразок обробки #n з #m.

Чи є спосіб я дізнатися значення m без повторення перед основною ітерацією?

Сподіваюся, я дав зрозуміти.

Відповіді:


338

IEnumerableне підтримує це. Це за дизайном. IEnumerableвикористовує ледачу оцінку, щоб отримати елементи, про які ви запитуєте, безпосередньо перед тим, як вони вам потрібні.

Якщо ви хочете дізнатися кількість елементів, не повторюючи їх, ви можете використовувати ICollection<T>, це має Countвластивість.


38
Я віддаю перевагу ICollection через IList, якщо вам не потрібно отримувати доступ до списку за індексом.
Майкл Медоуз

3
Зазвичай я просто хапаю Ліста та Іліста за звичку. Але особливо, якщо ви хочете їх реалізувати самостійно, ICollection простіше, а також має властивість Count. Дякую!
Mendelt

21
@Shimmy Ви повторюєте та рахуєте елементи. Або ви телефонуєте Count () з простору імен Linq, який робить це за вас.
Mendelt

1
Чи повинна просто заміна IEnumerable на IList бути достатньою за звичайних обставин?
Teekin

1
@Helgi Оскільки IEnumerable оцінюється ліниво, ви можете використовувати його для речей, які IList неможливо використовувати. Ви можете побудувати функцію, яка повертає IEnumerable, який перераховує, наприклад, усі децималі Pi. До тих пір, поки ви ніколи не намагаєтеся випереджати повний результат, він повинен просто працювати. Ви не можете створити IList, що містить Pi. Але це все досить академічно. Для більшості звичайних застосувань я повністю згоден. Якщо вам потрібен Count, вам потрібен 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 Більшість методів розширення в «Численному» мають оптимізацію для послідовностей різних типів, де вони використовуватимуть дешевший спосіб, якщо зможуть.
Шибумі

1
Згадане розширення доступне з .Net 3.5 та задокументовано в MSDN .
Крістіан

Я думаю , що вам не потрібен загальний типу Tна IEnumerator<T> enumerator = source.GetEnumerator(), просто ви можете зробити це: IEnumerator enumerator = source.GetEnumerator()і повинні працювати!
Джайдер

7
@Jaider - це трохи складніше за це. IEnumerable<T>успадковує, IDisposableщо дозволяє usingоператору автоматично розпоряджатися ним. IEnumerableне. Тож якщо ви зателефонуєте в GetEnumeratorбудь-якому випадку, вам слід закінчитиvar d = e as IDisposable; if (d != null) d.Dispose();
Деніел Ервікер

87

Просто додайте додаткову інформацію:

Count()Розширення не завжди ітерації. Розглянемо Linq до Sql, де підрахунок переходить до бази даних, але замість того, щоб повернути всі рядки, він видає SqlCount() і повертає цей результат замість цього.

Крім того, компілятор (або час виконання) досить розумний, що він буде викликати Count()метод об'єктів, якщо він має. Тож це не так, як кажуть інші респонденти, будучи абсолютно неосвіченими та завжди повторюючими, щоб рахувати елементи.

У багатьох випадках, коли програміст просто перевіряє 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()).
Nope

1
Перший пункт не є суто вірним ... LINQ для SQL використовує такий метод розширення, який не є таким, як цей . Якщо ви використовуєте друге, підрахунок виконується в пам'яті, а не як функція SQL
AlexFoxGill

1
@AlexFoxGill правильний. Якщо ви явно віддаєте свою IQueryably<T>команду, IEnumerable<T>вона не видасть sql. Коли я писав це, Linq до Sql був новим; Я думаю, що я просто намагався використовувати, Any()тому що для перелічених номерів, колекцій та sql це було набагато ефективніше і читабельніше (загалом). Дякуємо за вдосконалення відповіді.
Роберт Полсон

12

У мого друга є низка публікацій в блогах, які пояснюють, чому ви не можете цього зробити. Він створює функцію, яка повертає IEnumerable, коли кожна ітерація повертає наступне просте число, аж до ulong.MaxValueнаступного, і наступний елемент не обчислюється, поки ви не попросите цього. Швидке запитання: скільки предметів повертається?

Ось повідомлення, але вони такі довгі:

  1. Поза циклів (забезпечує початковий клас EnumerableUtility, який використовується в інших публікаціях)
  2. Застосування Iterate (Початкова реалізація)
  3. Божевільні методи витримки: ToLazyList (оптимізація продуктивності)

2
Я дуже хотів би, щоб MS визначила спосіб запитати перелічні працівники, щоб описати, що вони можуть щодо себе (при цьому "нічого не знаючи" є дійсною відповіддю). Ніхто з перелічених не повинен мати жодних труднощів відповідати на запитання на кшталт "Чи знаєте ви бути кінцевим", "Чи знаєте ви себе кінцевим з менш ніж N елементами" та "Чи знаєте ви себе нескінченним", оскільки будь-який перелік може законно (якщо недоброзичливо) відповісти "ні" всім. Якби були стандартні засоби для задавати такі запитання, то переписувачам було б набагато безпечніше повертати нескінченні послідовності ...
supercat

1
... (оскільки вони могли сказати, що вони так роблять), а для коду можна вважати, що перелічувачі, які не претендують на повернення нескінченних послідовностей, швидше за все, будуть обмежені. Зауважте, що включення засобів для запитання таких питань (щоб мінімізувати котельну табличку, ймовірно, мати об’єкт повернення EnumerableFeaturesоб’єкта), не вимагатиме від чисельників нічого складного, але спроможного задавати такі питання (поряд із деякими іншими на кшталт "Чи можете ви пообіцяти завжди повертайте ту саму послідовність елементів "," Чи можете ви безпечно потрапляти до коду, який не повинен змінювати вашу базову колекцію "тощо), було б дуже корисно.
supercat

Це було б досить круто, але я зараз впевнений, наскільки добре це поєднується з блоками ітераторів. Вам знадобляться якісь спеціальні "варіанти врожайності" чи щось таке. Або, можливо, використовувати атрибути для прикраси методу ітератора.
Джоель Куехорн

1
Блоки ітератора можуть, за відсутності будь-яких інших спеціальних декларацій, просто повідомити, що вони нічого не знають про повернуту послідовність, хоча якщо IEnumerator або спадкоємець, схвалений MS (що може бути повернуто реалізаціями GetEnumerator, які знали про його існування) повинні були підтримувати додаткову інформацію, C #, ймовірно, отримає set yield optionsзаяву або щось подібне, щоб підтримати її. Якщо правильно розроблено, IEnhancedEnumeratorможе зробити такі речі, як LINQ, набагато кориснішими, усунувши багато "оборонних" ToArrayабо ToListвикликів, особливо ...
supercat

... у випадках, коли подібні речі Enumerable.Concatвикористовуються для комбінування великої колекції, яка багато чого знає про себе, і маленької, яка ні.
supercat

10

Численні не можуть рахувати без повторення.

За "звичайних" обставин, для класів, що реалізують IEnumerable або IEnumerable <T>, наприклад List <T>, можна було б реалізувати метод Count шляхом повернення властивості List <T> .Count. Однак метод Count насправді не є методом, визначеним в інтерфейсі IEnumerable <T> або IEnumerable. (Єдиним, що насправді є GetEnumerator.) А це означає, що для нього не може бути передбачена специфічна для класу реалізація.

Скоріше, Count - це метод розширення, визначений у статичному класі Enumerable. Це означає, що його можна викликати в будь-якому екземплярі похідного IEnumerable <T> класу, незалежно від реалізації цього класу. Але це також означає, що він реалізований в одному місці, зовнішньому для будь-якого з цих класів. Що, звичайно, означає, що вона повинна бути реалізована таким чином, що повністю не залежить від внутрішніх класів цього класу. Єдиний такий спосіб підрахунку - це через ітерацію.


це хороший момент, коли ви не можете рахувати, якщо ви не повторите. Функціонал підрахунку прив’язаний до класів, які реалізують IEnumerable .... таким чином, ви повинні перевірити, який тип IEnumerable є вхідним (перевірити кастингом), і тоді ви знаєте, що у списку <> та словнику <> є певні способи підрахунку та використання ті лише після того, як ви дізнаєтесь тип. Мені ця тема була дуже корисною особисто, тому дякую за вашу відповідь, Кріс.
PositiveGuy

1
Відповідно до відповіді Даніеля, ця відповідь не є строго вірною: реалізація НЕ перевіряє, чи об’єкт реалізує "ICollection", у якому є поле "Count". Якщо так, він використовує це. (Чи було це розумним у '08 році, я не знаю.)
ToolmakerSteve


8

Ні, не взагалі. Одним із моментів використання перелічувальних даних є те, що фактичний набір об'єктів у перерахунку не відомий (заздалегідь або навіть взагалі).


Важливим моментом, який ви підняли, є те, що навіть коли ви отримуєте цей об'єкт 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".


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 {..код для всіх, але вперше ..} ...} "Справді, це незграбніше, ніж ваш простий підхід, коли одноразовий код був би всередині вашого, якщо до циклу, але якщо вартість" .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


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.
Гюго


-3

Я використовую, IEnum<string>.ToArray<string>().Lengthі це чудово працює.


Це має добре працювати. IEnumerator <Object> .ToArray <Object> .Length
din

1
Чому ви робите це, а не більш лаконічне та швидше виконання рішення, вже висловлене у високообоснованій відповіді Даніеля, написаній на три роки раніше, ніж у вас , " IEnum<string>.Count();"?
ToolmakerSteve

Ти правий. Я якось не помітив відповідь Даніеля, можливо, тому, що він цитував реалізацію, і я подумав, що він реалізує метод розширення, і я шукав рішення з меншим кодом.
Олівер Кьоттер

Який найбільш прийнятний спосіб поводження з моєю поганою відповіддю? Чи слід його видалити?
Олівер Кьоттер

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.