Практичне використання ключового слова "урожайність" у C # [закрито]


76

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


9
Всі (або принаймні більшість) LINQ реалізовані з використанням урожайності. Також рамка Unity3D знайшла для цього корисну користь - вона використовується для призупинення функцій (у виписках про вихід) та відновлення її пізніше, використовуючи стан в IEnumerable.
Дани

2
Чи не слід це переміщувати до StackOverflow?
Danny Varod

4
@Danny - Це не підходить для переповнення стека, оскільки питання не вимагає вирішення конкретної проблеми, а запитання про те, yieldдля чого взагалі можна використовувати.
ChrisF

9
Насправді? Я не можу придумати жодного додатка, де я не використовував його.
Aaronaught

Відповіді:


107

Ефективність

yieldКлючове слово ефективно створює ліниве перерахування по пунктам збору , які можуть бути набагато більш ефективним. Наприклад, якщо ваш foreachцикл повторює лише перші 5 елементів з 1 мільйона предметів, то це все yieldповертається, і ви спочатку не створили колекцію з 1 мільйона предметів. Так само ви хочете використовувати yieldз IEnumerable<T>поверненими значеннями у власних сценаріях програмування для досягнення тієї ж ефективності.

Приклад ефективності, досягнутої за певним сценарієм

Не метод ітератора, потенційне неефективне використання великої колекції
(Проміжний збірник побудований з великою кількістю предметів)

// Method returns all million items before anything can loop over them. 
List<object> GetAllItems() {
    List<object> millionCustomers;
    database.LoadMillionCustomerRecords(millionCustomers); 
    return millionCustomers;
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in GetAllItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: One million items returned, but only 5 used. 

Версія ітератора, ефективна
(проміжний збірник не побудований)

// Yields items one at a time as the caller's foreach loop requests them
IEnumerable<object> IterateOverItems() {
    for (int i; i < database.Customers.Count(); ++i)
        yield return database.Customers[i];
}

// MAIN example ---------------------
// Caller code sample:
int num = 0;
foreach(var itm in IterateOverItems())  {
    num++;
    if (num == 5)
        break;
}
// Note: Only 5 items were yielded and used out of the million.

Спростіть деякі сценарії програмування

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

Лише одним із прикладів є злиття двох списків:

IEnumerable<object> EfficientMerge(List<object> list1, List<object> list2) {
    foreach(var o in list1) 
        yield return o; 
    foreach(var o in list2) 
        yield return o;
}

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

Більше інформації

yieldКлючове слово може бути використано тільки в контексті методу ітератора (має тип повернення IEnumerable, IEnumerator, IEnumerable<T>, або IEnumerator<T>.) , І є особливі відносини з foreach. Ітератори - це спеціальні методи. Документація про вихід MSDN та документація ітераторів містять багато цікавої інформації та пояснень понять. Переконайтеся , співвіднести його з по foreachключовим словом , прочитавши про це теж, щоб доповнити ваше розуміння ітератори.

Щоб дізнатися про те, як ітератори досягають своєї ефективності, секрет полягає в коді IL, згенерованому компілятором C #. ІЛ, генерований для методу ітератора, різко відрізняється від того, що створюється для звичайного (не ітератора) методу. Ця стаття (Що насправді генерує ключове слово ?) Надає таке розуміння.


2
Вони особливо корисні для алгоритмів, які беруть (можливо, довгу) послідовність і генерують інший, де відображення не однозначне. Прикладом цього є відсікання багатокутника; будь-який конкретний край може генерувати безліч або навіть жодних країв, одного разу обрізаних. Ітератори роблять це надзвичайно легше виразити, а урожайність - один з найкращих способів їх написання.
Стипендіати Дональ

+1 Набагато краща відповідь, як я записав. Тепер я також дізнався, що врожайність хороша для кращих показників.
Jan_V

3
Колись я використовував урожай для створення пакетів для бінарного мережевого протоколу. Це здавалося найбільш природним вибором у C #.
György Andrasek

4
Чи не database.Customers.Count()перераховується ціле перерахування клієнтів, вимагаючи, таким чином, більш ефективного коду для проходження кожного товару?
Стівен

5
Називай мене анальним, але це конкатенація, а не злиття. (І у Linq вже є метод Concat.)
OldFart

4

Деякий час тому у мене був практичний приклад, припустимо, у вас така ситуація:

List<Button> buttons = new List<Button>();
void AddButtons()
{
   for ( int i = 0; i <= 10; i++ ) {
      var button = new Button();
      buttons.Add(button);
      button.Click += (sender, e) => 
          MessageBox.Show(String.Format("You clicked button number {0}", ???));
   }
}

Об'єкт кнопки не знає власної позиції в колекції. Це ж обмеження стосується Dictionary<T>або інших колекцій.

Ось моє рішення за допомогою yieldключового слова:

interface IHasId { int Id { get; set; } }

class IndexerList<T>: List<T>, IEnumerable<T> where T: IHasId
{
   List<T> elements = new List<T>();
   new public void Clear() { elements.Clear(); }
   new public void Add(T element) { elements.Add(element); }
   new public int Count { get { return elements.Count; } }    
   new public IEnumerator<T> GetEnumerator()
   {
      foreach ( T c in elements )
         yield return c;
   }

   new public T this[int index]
   {
      get
      {
         foreach ( T c in elements ) {
            if ( (int)c.Id == index )
               return c;
         }
         return default(T);
      }
   }
}

І ось як я цим користуюся:

class ButtonWithId: Button, IHasId
{
   public int Id { get; private set; }
   public ButtonWithId(int id) { this.Id = id; }
}

IndexerList<ButtonWithId> buttons = new IndexerList<ButtonWithId>();
void AddButtons()
{
   for ( int i = 10; i <= 20; i++ ) {
      var button = new ButtonWithId(i);
      buttons.Add(button);
      button.Click += (sender, e) => 
         MessageBox.Show(String.Format("You clicked button number {0}", ( (ButtonWithId)sender ).Id));
   }
}

Мені не потрібно робити forцикл над своєю колекцією, щоб знайти індекс. Моя кнопка має ідентифікатор, і це також використовується в якості індексу IndexerList<T>, тому ви уникаєте зайвих ідентифікаторів або індексів - ось що мені подобається! Індекс / Id може бути довільним числом.


2

Тут можна знайти практичний приклад:

http://www.ytechie.com/2009/02/using-c-yield-for-readability-and-performance.html

Є ряд переваг використання урожайності над стандартним кодом:

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

Однак, як сказав Jan_V (просто побий мене на це декількома секундами :-), ти можеш жити без цього, оскільки внутрішньо компілятор видасть код майже однаковий в обох випадках.


1

Ось приклад:

https://bitbucket.org/ant512/workingweek/src/a745d02ba16f/source/WorkingWeek/Week.cs#cl-158

Клас виконує обчислення дати на основі робочого тижня. Я можу сказати приклад класу, що Боб працює з 9:30 до 17:30 щотижня з перервою на годину обіду о 12:30. За допомогою цих знань функція AscendingShifts () дасть об'єкти робочої зміни між наданими датами. Щоб перерахувати всі робочі зрушення Боба між 1 січня та 1 лютого цього року, ви використовуєте це так:

foreach (var shift in week.AscendingShifts(new DateTime(2011, 1, 1), new DateTime(2011, 2, 1)) {
    Console.WriteLine(shift);
}

Клас насправді не перебирає колекцію. Однак зміщення між двома датами можна розглядати як колекцію. yieldОператор дозволяє перебрати цю уявну колекцію без створення самої колекції.


1

У мене є невеликий рівень даних db, у якому є commandклас, у якому ви встановлюєте текст команди SQL, тип команди та повертаєте IEnumerable з «параметрів команди».

В основному ідея полягає в тому, щоб SqlCommandвесь час вводити команди CLR замість ручного заповнення властивостей та параметрів.

Отже, є функція, яка виглядає приблизно так:

IEnumerable<DbParameter> GetParameters()
{
    // here i do something like

    yield return new DbParameter { name = "@Age", value = this.Age };

    yield return new DbParameter { name = "@Name", value = this.Name };
}

Клас, який успадковує цей commandклас, має властивості Ageта Name.

Потім ви можете створити commandоб'єкт, заповнений його властивостями, і передати його в dbінтерфейс, який фактично виконує команду.

Загалом, це дуже просто працювати з командами SQL та підтримувати їх набір.


1

Хоча випадок злиття вже висвітлювався у прийнятій відповіді, дозвольте мені показати вам метод розширення параметрів виходу та злиття ™:

public static IEnumerable<T> AppendParams<T>(this IEnumerable<T> a, params T[] b)
{
    foreach (var el in a) yield return el;
    foreach (var el in b) yield return el;
}

Я використовую це для створення пакетів мережевого протоколу:

static byte[] MakeCommandPacket(string cmd)
{
    return
        header
        .AppendParams<byte>(0, 0, 1, 0, 0, 1, 0x92, 0, 0, 0, 0)
        .AppendAscii(cmd)
        .MarkLength()
        .MarkChecksum()
        .ToArray();
}

Наприклад, MarkChecksumметод виглядає приблизно так. І він також має yield:

public static IEnumerable<byte> MarkChecksum(this IEnumerable<byte> data, int pos = 6)
{
    foreach (byte b in data)
    {
        yield return pos-- == 0 ? (byte)data.Sum(z => z) : b;
    }
}

Але будьте обережні при використанні сукупних методів, таких як Sum () в методі перерахування, оскільки вони викликають окремий процес перерахування.


1

Приклад Elastic Search .NET repo має чудовий приклад використання yield returnдля розподілу колекції на кілька колекцій із заданим розміром:

https://github.com/elastic/elasticsearch-net-example/blob/master/src/NuSearch.Domain/Extensions/PartitionExtension.cs

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int size)
    {
        T[] array = null;
        int count = 0;
        foreach (T item in source)
        {
            if (array == null)
            {
                array = new T[size];
            }
            array[count] = item;
            count++;
            if (count == size)
            {
                yield return new ReadOnlyCollection<T>(array);
                array = null;
                count = 0;
            }
        }
        if (array != null)
        {
            Array.Resize(ref array, count);
            yield return new ReadOnlyCollection<T>(array);
        }
    }

0

Розгортаючись на відповідь Jan_V, я просто потрапив у реальний випадок, пов’язаний із цим:

Мені потрібно було використовувати версії Kernel32 FindFirstFile / FindNextFile. Ви отримуєте ручку з першого дзвінка і подаєте її на всі наступні дзвінки. Загорніть це в перелік, і ви отримаєте щось, що можете безпосередньо використати з foreach.

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