Яка мета / перевага використання ітераторів повернення прибутковості в C #?


80

Всі приклади, які я бачив із використанням yield return x; методу C #, можна зробити так само, просто повернувши весь список. У цих випадках, чи є якась перевага чи перевага у використанні yield returnсинтаксису проти повернення списку?

Крім того, у яких типах сценаріїв yield returnбудуть використовуватися такі, що ви не можете просто повернути повний список?


15
Чому ви припускаєте, що спочатку існує "список"? Що робити, якщо немає?
Ерік Ліпперт,

2
@ Ерік, мабуть, це я просив. Коли б у вас взагалі не було списку. Потоки файлів і нескінченні послідовності є 2 чудовими прикладами відповідей на даний момент.
CoderDennis

1
Якщо у вас є список, звичайно, просто поверніть його; але якщо ви будуєте список усередині методу і повертаєте його, тоді ви могли б / повинні використовувати ітератори замість цього. Віддавайте предмети по одному. Є багато переваг.
justin.m.покупка

2
Звичайно, я багато чому навчився, поставивши це питання 5 років тому!
CoderDennis

1
Найбільшою перевагою наявності yields є те, що вам не потрібно називати ще одну проміжну змінну.
nawfal

Відповіді:


120

Але що, якби ви самі будували колекцію?

Загалом, ітератори можна використовувати для лінивого генерування послідовності об’єктів . Наприклад, Enumerable.Rangeметод не має ніякого збору всередині. Він просто генерує наступне число на вимогу . Існує багато способів використання цієї лінивої генерації послідовностей за допомогою автомата стану. Більшість з них охоплені концепціями функціонального програмування .

На мій погляд, якщо ви розглядаєте ітератори лише як спосіб перерахувати колекцію (це лише один з найпростіших випадків використання), ви підете неправильним шляхом. Як я вже сказав, ітератори є засобом для повернення послідовностей. Послідовність може бути навіть нескінченною . Неможливо повернути список із нескінченною довжиною та використати перші 100 елементів. Іноді доводиться лінуватися. Повернення колекції значно відрізняється від повернення генератора колекції (що є ітератором). Це порівняння яблук з апельсинами.

Гіпотетичний приклад:

static IEnumerable<int> GetPrimeNumbers() {
   for (int num = 2; ; ++num) 
       if (IsPrime(num))
           yield return num;
}

static void Main() { 
   foreach (var i in GetPrimeNumbers()) 
       if (i < 10000)
           Console.WriteLine(i);
       else
           break;
}

Цей приклад друкує прості числа менше 10000. Ви можете легко змінити його, щоб надрукувати числа менше мільйона, не торкаючись алгоритму генерації простих чисел. У цьому прикладі ви не можете повернути список усіх простих чисел, оскільки послідовність нескінченна, і споживач навіть не знає, скільки предметів він хоче з самого початку.


Правильно. Я створив список, але скільки різниться для повернення по одному елементу за раз проти повернення цілого списку?
CoderDennis

4
Серед інших причин це робить ваш код більш модульним, щоб ви могли завантажувати елемент, обробляти, а потім повторювати. Крім того, розглянемо випадок, коли завантаження товару є дуже дорогим, або їх багато (кажуть мільйони). У цих випадках завантаження всього списку небажано.
Dana Sane

15
@Dennis: Для лінійно збереженого списку в пам'яті він може не мати різниці, але якби ви, наприклад, перераховували файл розміром 10 ГБ і обробляли кожен рядок по одному, це мало б значення.
mmx

1
+1 за чудову відповідь - я б також додав, що ключове слово yield дозволяє застосовувати ітераторну семантику до джерел, які традиційно не вважаються колекціями - таких як мережеві сокети, веб-служби або навіть проблеми з паралельністю (див. Stackoverflow.com/questions/ 481714 / ccr-yield-and-vb-net )
Л.Бушкін

Приємний приклад, тому в основному це генератор колекцій, який базується на контексті (наприклад, виклик методу) і не запускається до дії, поки щось не спробує отримати до нього доступ, тоді як традиційний метод збору без доходу повинен знати його розмір для побудови і повернути повну колекцію - потім виконати ітерацію необхідної частини цієї колекції?
Michael Harper

24

Точні відповіді тут свідчать про те, що перевага yield returnполягає в тому, що вам не потрібно створювати список ; Списки можуть бути дорогими. (Крім того, через деякий час ви виявите їх громіздкими та неелегантними.)

Але що, якщо у вас немає Списку?

yield return дозволяє здійснювати траверс структури даних (не обов’язково Списки) різними способами. Наприклад, якщо ваш об'єкт є Деревом, ви можете обходити вузли в попередньому або післязамовленні без створення інших списків або зміни базової структури даних.

public IEnumerable<T> InOrder()
{
    foreach (T k in kids)
        foreach (T n in k.InOrder())
            yield return n;
    yield return (T) this;
}

public IEnumerable<T> PreOrder()
{
    yield return (T) this;
    foreach (T k in kids)
        foreach (T n in k.PreOrder())
            yield return n;
}

1
Цей приклад також висвітлює випадок делегування. Якщо у вас є колекція, яка за певних обставин може містити предмети інших колекцій, дуже просто виконати ітерацію та використовувати return return замість того, щоб створювати повний список усіх результатів і повертати це.
Том Мейфілд,

1
Тепер C # потрібно просто реалізувати yield!так, як це робить F #, так що вам не потрібні всі foreachтвердження.
CoderDennis

До речі, ваш приклад показує одну з "небезпек" yield return: часто не очевидно, коли він дасть ефективний або неефективний код. Хоча yield returnйого можна використовувати рекурсивно, таке використання покладе значні накладні витрати на обробку глибоко вкладених перечислювачів. Ручне управління станом може бути складніше кодувати, але працювати набагато ефективніше.
supercat

17

Ледача оцінка / відкладене виконання

Блоки ітераторів "return yield" не виконуватимуть жодного з кодів, поки ви насправді не покличете для цього конкретного результату. Це означає, що їх також можна ефективно поєднати. Спільна вікторина: скільки разів наступний код повторюватиме файл?

var query = File.ReadLines(@"C:\MyFile.txt")
                            .Where(l => l.Contains("search text") )
                            .Select(l => int.Parse(l.SubString(5,8))
                            .Where(i => i > 10 );

int sum=0;
foreach (int value in query) 
{
    sum += value;
}

Відповідь рівно одна, і це лише до того моменту, поки це не буде foreach. Незважаючи на те, що у мене є три окремі функції оператора linq, ми все одно прокручуємо вміст файлу лише один раз.

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

Нескінченні списки

Дивіться мою відповідь на це запитання як хороший приклад:
функція C # Фібоначчі повертає помилки

По суті, я реалізую послідовність Фібоначчі за допомогою блоку ітераторів, який ніколи не зупиниться (принаймні, не раніше, ніж досягти MaxInt), а потім використовую цю реалізацію безпечним способом.

Покращена семантика та відокремлення питань

Знову ж таки, використовуючи приклад файлу зверху, тепер ми можемо легко відокремити код, який зчитує файл, від коду, який відфільтровує непотрібні рядки з коду, який насправді аналізує результати. Цей перший, особливо, дуже багаторазовий.

Це одна з тих речей, яку набагато важче пояснити прозою, ніж просто комусь із простим візуальним 1 :

Імператив проти функціонального розділення проблем

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


1 Я вважаю, що це першоджерело: https://twitter.com/mariofusco/status/571999216039542784 . Також зверніть увагу, що цей код - Java, але C # буде подібним.


1
Відкладене виконання - це, мабуть, найбільша перевага ітераторів.
justin.m.покупка

12

Іноді послідовності, які потрібно повернути, просто занадто великі, щоб поміститися в пам’яті. Наприклад, близько 3 місяців тому я брав участь у проекті з міграції даних між базами даних MS SLQ. Дані експортувались у форматі XML. Повернення прибутковості виявилось досить корисним із XmlReader . Це значно спростило програмування. Наприклад, припустимо, у файлі було 1000 елементів Клієнта - якщо ви просто прочитаєте цей файл у пам’яті, для цього потрібно буде зберегти всі їх в пам’яті одночасно, навіть якщо вони обробляються послідовно. Отже, ви можете використовувати ітератори для того, щоб обходити колекцію по черзі. У такому випадку вам доведеться витратити лише пам’ять на один елемент.

Як виявилося, використання XmlReader для нашого проекту було єдиним способом змусити програму працювати - вона працювала тривалий час, але принаймні не зависала вся система і не викликала OutOfMemoryException . Звичайно, ви можете працювати з XmlReader без ітераторів врожаю. Але ітератори значно полегшили мені життя (я не писав би код для імпорту так швидко і без проблем). Перегляньте цю сторінку , щоб побачити, як ітератори врожайності використовуються для вирішення реальних проблем (а не лише наукових з нескінченними послідовностями).


9

У сценаріях іграшок / демонстрацій немає великої різниці. Але бувають ситуації, коли вихідні ітератори корисні - іноді весь список недоступний (наприклад, потоки), або список обчислювально дорогий і навряд чи потрібен у повному обсязі.


2

Якщо весь список гігантський, він може з’їсти багато пам’яті, щоб просто посидіти, тоді як із врожайністю ви граєте лише з тим, що вам потрібно, коли вам це потрібно, незалежно від того, скільки предметів є.



2

Використовуючи, yield returnви можете переглядати елементи, не створюючи список. Якщо вам не потрібен список, але ви хочете переглядати певний набір елементів, це може бути простіше написати

foreach (var foo in GetSomeFoos()) {
    operate on foo
}

Ніж

foreach (var foo in AllFoos) {
    if (some case where we do want to operate on foo) {
        operate on foo
    } else if (another case) {
        operate on foo
    }
}

Ви можете покласти всю логіку для визначення того, чи хочете ви оперувати foo всередині свого методу, використовуючи повернення доходу, і ваш цикл foreach може бути набагато більш стислим.


2

Ось моя попередня прийнята відповідь на те саме питання:

Прибутковість ключового слова додана?

Інший спосіб поглянути на методи ітераторів полягає в тому, що вони виконують важку роботу, перетворюючи алгоритм «навиворіт». Розглянемо парсер. Він витягує текст із потоку, шукає в ньому шаблони та формує логічний опис вмісту на високому рівні.

Тепер я можу зробити це легким для себе як автора синтаксичного аналізатора, застосовуючи підхід SAX, в якому у мене є інтерфейс зворотного виклику, про який я повідомляю, коли знаходжу наступний фрагмент шаблону. Отже, у випадку SAX, кожного разу, коли я знаходжу початок елемента, я викликаю beginElementметод тощо.

Але це створює проблеми для моїх користувачів. Вони повинні реалізувати інтерфейс обробника, і тому вони повинні написати клас автомата стану, який реагує на методи зворотного виклику. Це важко виправити, тому найпростіше, що потрібно зробити, це скористатися реалізацією запасів, яка будує дерево DOM, і тоді їм буде зручно ходити по дереву. Але потім вся структура буферизується в пам'яті - недобре.

Але як щодо того, щоб замість цього я написав свій парсер як метод ітератора?

IEnumerable<LanguageElement> Parse(Stream stream)
{
    // imperative code that pulls from the stream and occasionally 
    // does things like:

    yield return new BeginStatement("if");

    // and so on...
}

Це буде не важче написати, ніж підхід до інтерфейсу зворотного виклику - просто поверніть об’єкт, похідний від мого LanguageElementбазового класу, замість того, щоб викликати метод зворотного виклику.

Тепер користувач може використовувати foreach для перегляду результатів мого синтаксичного аналізатора, тому він отримує дуже зручний обов’язковий інтерфейс програмування.

Результат полягає в тому, що обидві сторони користувацького API виглядають так, ніби вони контролюють , а отже їх легше писати та розуміти.


2

Основною причиною використання yield є те, що воно генерує / повертає список самостійно. Ми можемо використовувати повернутий список для подальшої ітерації.


Концептуально правильний, але технічно неправильний. Він повертає екземпляр IEnumerable, який просто абстрагує ітератор. Цей ітератор насправді є логікою отримання наступного елемента, а не матеріалізованим списком. Використання return yieldне генерує список, воно генерує лише наступний елемент у списку і лише за запитом (повторення).
Sinaesthetic
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.