Практическое использование ключевого слова yield в C # [закрыто]

76

После почти 4 лет опыта я не видел кода, в котором используется ключевое слово yield . Может ли кто-нибудь показать мне практическое использование (наряду с объяснением) этого ключевого слова, и если да, то нет ли других способов, чтобы легче было выполнить то, что оно может сделать?

Саид Нямати
источник
9
Все (или, по крайней мере, большинство) из LINQ реализованы с использованием yield. Кроме того, инфраструктура Unity3D нашла для нее хорошее применение - она ​​используется для приостановки функций (в операторах yield) и возобновления ее позже, используя состояние в IEnumerable.
Дани
2
Разве это не должно быть перемещено в StackOverflow?
Дэнни Варод
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 #. IL, сгенерированный для метода итератора, кардинально отличается от того, который генерируется для обычного (не итераторного) метода. В этой статье («Что в действительности генерирует ключевое слово доходность?») Дается такое понимание.

Джон К
источник
2
Они особенно полезны для алгоритмов, которые берут (возможно длинную) последовательность и генерируют другую, в которой отображение не однозначно. Примером этого является обрезка полигонов; любое конкретное ребро может генерировать много или даже не ребра после обрезки. Итераторы делают это чрезвычайно легко выразить, и сдача является одним из лучших способов их написания.
Донал Феллоуз
+1 Гораздо лучше ответь как я записал. Теперь я также узнал, что урожайность хороша для лучшей производительности.
Jan_V
3
Когда-то я использовал yield для создания пакетов для двоичного сетевого протокола. Это казалось наиболее естественным выбором в C #.
Георгий Андрасек
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

Существует несколько преимуществ использования yield по сравнению со стандартным кодом:

  • Если для построения списка используется итератор, вы можете получить возвращаемое значение, и вызывающий объект может решить , хочет ли он получить этот результат в списке или нет.
  • Вызывающий также может решить отменить итерацию по причине, которая выходит за рамки того, что вы делаете в итерации.
  • Код немного короче.

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

Jalayn
источник
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

У меня есть небольшой уровень данных БД, в котором есть commandкласс, в котором вы устанавливаете текст команды SQL, тип команды и возвращаете IEnumerable из «параметров команды».

По сути, идея состоит в том, чтобы набирать команды CLR вместо того, чтобы вручную заполнять SqlCommandсвойства и параметры все время.

Итак, есть функция, которая выглядит так:

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

Хотя случай слияния уже был рассмотрен в принятом ответе, позвольте мне показать вам метод расширения параметров yield-merge params ™:

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 есть отличный пример использования 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);
        }
    }
pholly
источник
0

Разбираясь с ответом Jan_V, я попал в реальный случай, связанный с ним:

Мне нужно было использовать Kernel32 версии FindFirstFile / FindNextFile. Вы получаете ручку от первого звонка и подаете его на все последующие звонки. Оберните это в перечислитель, и вы получите то, что можете напрямую использовать с foreach.

Лорен Печтель
источник