Правильное использование «доходности»

794

yield ключевое слово является одним из тех keywords в С#, который продолжает меня мистифицировать, и я никогда не был уверен, что правильно его использую.

Из следующих двух частей кода, который является предпочтительным и почему?

Версия 1: Использование возврата возврата

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        foreach (Product product in products)
        {
            yield return product;
        }
    }
}

Версия 2: Вернуть список

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList<Product>();
    }
}
  • 31
    yield привязан к IEnumerable<T> и его виду. Это в некотором роде ленивая оценка
  • 0
    Вот отличный ответ на аналогичный вопрос. stackoverflow.com/questions/15381708/...
Показать ещё 3 комментария
Теги:
yield-return

17 ответов

762
Лучший ответ

Я обычно использую return-return при вычислении следующего элемента в списке (или даже следующей группы элементов).

Используя вашу версию 2, перед возвратом у вас должен быть полный список. Используя return-return, вам действительно нужно иметь следующий элемент перед возвратом.

Помимо всего прочего, это помогает распределить вычислительную стоимость сложных вычислений на более высокий временной интервал. Например, если список подключен к графическому интерфейсу и пользователь никогда не переходит на последнюю страницу, вы никогда не вычисляете конечные элементы в списке.

Другой случай, когда yield-return предпочтительнее, - это если IEnumerable представляет бесконечный набор. Рассмотрим список простых чисел или бесконечный список случайных чисел. Вы никогда не сможете вернуть полный IEnumerable сразу, поэтому вы используете return-return, чтобы возвращать список пошагово.

В вашем конкретном примере у вас есть полный список продуктов, поэтому я бы использовал версию 2.

  • 29
    Я бы заметил, что в вашем примере в вопросе 3 объединены два преимущества. 1) Это распределяет вычислительные затраты (иногда выгода, иногда нет) 2) Это может лениво избегать вычислений бесконечно во многих случаях использования. Вы не упомянули о потенциальном недостатке, заключающемся в промежуточном состоянии. Если у вас есть значительное количество промежуточного состояния (скажем, HashSet для удаления дубликатов), то использование yield может привести к увеличению объема используемой памяти.
  • 8
    Кроме того, если каждый отдельный элемент очень большой, но к ним нужно обращаться только последовательно, доходность лучше.
Показать ещё 4 комментария
510

Заполнение временного списка похоже на загрузку всего видео, тогда как использование yield похоже на потоковое видео.

  • 158
    Я прекрасно понимаю, что этот ответ не является техническим ответом, но я считаю, что сходство между yield и потоковым видео служит хорошим примером при понимании ключевого слова yield. Все техническое уже было сказано по этому вопросу, поэтому я попытался объяснить «другими словами». Существует ли правило сообщества, которое гласит, что вы не можете объяснить свои идеи в нетехнических терминах?
  • 11
    Я не уверен, кто за вас проголосовал или почему (я хотел бы, чтобы они это прокомментировали), но я думаю, что это несколько описывает это с нетехнической точки зрения.
Показать ещё 3 комментария
59

В качестве концептуального примера для понимания, когда вы должны использовать yield, скажем, метод ConsumeLoop() обрабатывает элементы, возвращенные/предоставленные ProduceList():

void ConsumeLoop() {
    foreach (Consumable item in ProduceList())        // might have to wait here
        item.Consume();
}

IEnumerable<Consumable> ProduceList() {
    while (KeepProducing())
        yield return ProduceExpensiveConsumable();    // expensive
}

Без yield вызов ProduceList() может занять много времени, потому что вы должны завершить список перед возвратом:

//pseudo-assembly
Produce consumable[0]                   // expensive operation, e.g. disk I/O
Produce consumable[1]                   // waiting...
Produce consumable[2]                   // waiting...
Produce consumable[3]                   // completed the consumable list
Consume consumable[0]                   // start consuming
Consume consumable[1]
Consume consumable[2]
Consume consumable[3]

Используя yield, он становится перегруппированным, вроде как "параллельно":

//pseudo-assembly
Produce consumable[0]
Consume consumable[0]                   // immediately Consume
Produce consumable[1]
Consume consumable[1]                   // consume next
Produce consumable[2]
Consume consumable[2]                   // consume next
Produce consumable[3]
Consume consumable[3]                   // consume next

И, наконец, как уже было сказано ранее, вы должны использовать версию 2, потому что у вас уже есть заполненный список.

25

Это будет похоже на странное предложение, но я узнал, как использовать ключевое слово yield в С#, прочитав презентацию о генераторах в Python: David M. Beazley http://www.dabeaz.com/generators/Generators.pdf. Вам не нужно много знать Python, чтобы понять презентацию - я этого не сделал. Мне было очень полезно объяснить не только то, как работают генераторы, но и почему вы должны заботиться.

  • 1
    Презентация предоставляет простой обзор. Детали того, как это работает в C #, обсуждаются Рэем Ченом в ссылках на stackoverflow.com/a/39507/939250 . Первая ссылка подробно объясняет, что есть второй, неявный возврат в конце методов возврата доходности.
23

Я знаю, что это старый вопрос, но я хотел бы предложить один пример того, как ключевое слово yield может быть творчески использовано. Я действительно выиграл от этой техники. Надеюсь, это поможет любому, кто споткнется на этот вопрос.

Примечание. Не думайте о ключевом слове yield как просто другом способе создания коллекции. Большая часть мощности урожая приходит в том, что выполнение приостановлено в вашем метод или свойство до тех пор, пока вызывающий код не повторится по следующему значению. Вот мой пример:

Используя ключевое слово yield (наряду с Rob Eisenburg реализация Caliburn.Micro coroutines), я могу выразить асинхронный вызов веб-сервису следующим образом:

public IEnumerable<IResult> HandleButtonClick() {
    yield return Show.Busy();

    var loginCall = new LoginResult(wsClient, Username, Password);
    yield return loginCall;
    this.IsLoggedIn = loginCall.Success;

    yield return Show.NotBusy();
}

Что это будет делать, это включить мой BusyIndicator, вызвать метод Login в моей веб-службе, установить флаг IsLoggedIn в возвращаемое значение и затем отключить BusyIndicator.

Вот как это работает: IResult имеет метод Execute и событие Completed. Caliburn.Micro захватывает IEnumerator от вызова HandleButtonClick() и передает его в метод Coroutine.BeginExecute. Метод BeginExecute начинает итерацию через IResults. Когда возвращается первый IResult, выполнение приостанавливается внутри HandleButtonClick(), а BeginExecute() присоединяет обработчик событий к событию Completed и вызывает Execute(). IResult.Execute() может выполнять либо синхронную, либо асинхронную задачу и запускает событие Completed, когда оно завершено.

LoginResult выглядит примерно так:

public LoginResult : IResult {
    // Constructor to set private members...

    public void Execute(ActionExecutionContext context) {
        wsClient.LoginCompleted += (sender, e) => {
            this.Success = e.Result;
            Completed(this, new ResultCompletionEventArgs());
        };
        wsClient.Login(username, password);
    }

    public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    public bool Success { get; private set; }
}

Это может помочь настроить что-то подобное и выполнить выполнение, чтобы посмотреть, что происходит.

Надеюсь, это поможет кому-то! Мне очень понравилось исследовать различные способы использования урожая.

12

Две части кода действительно делают две разные вещи. Первая версия будет тянуть участников по мере необходимости. Вторая версия будет загружать все результаты в память, прежде чем вы начнете что-либо делать с ней.

Нет правильного или неправильного ответа на этот вопрос. Какой из них предпочтительнее, просто зависит от ситуации. Например, если у вас есть время, необходимое для завершения запроса, и вам нужно сделать что-то полузасушливое с результатами, вторая версия может быть предпочтительной. Но будьте осторожны с большими наборами результатов, особенно если вы используете этот код в 32-битном режиме. Несколько раз я был укушен исключениями OutOfMemory при выполнении этого метода.

Главное, что нужно помнить, это: различия в эффективности. Таким образом, вы, вероятно, должны пойти с тем, что делает ваш код более простым и изменить его только после профилирования.

10

Вот что Chris Sells рассказывает об этих утверждениях в языке программирования С#;

Иногда я забываю, что доходность возврата не совпадает с возвратом, в что код после возврата доходности может быть выполнен. Например, код после первого возврата здесь никогда не может быть выполнен:

    int F() {
return 1;
return 2; // Can never be executed
}

Напротив, код после первого возврата возврата здесь может быть выполняется:

IEnumerable<int> F() {
yield return 1;
yield return 2; // Can be executed
}

Это часто укусит меня в выражении if:

IEnumerable<int> F() {
if(...) { yield return 1; } // I mean this to be the only
// thing returned
yield return 2; // Oops!
}

В этих случаях, помня, что возврат доходности не является "окончательным", как возвращение полезно.

  • 0
    чтобы сократить двусмысленность, уточните, когда вы говорите, может, это будет или может быть? может ли первый возвратиться, а не выполнить второй выход?
10

Выход имеет два больших использования

Это помогает обеспечить пользовательскую итерацию без создания временных наборов. (загрузка всех данных и цикл)

Это помогает делать итерацию с сохранением состояния. (потоковая передача)

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

http://www.youtube.com/watch?v=4fju3xcm21M

9

Возврат доходности может быть очень сильным для алгоритмов, где вам нужно перебирать миллионы объектов. Рассмотрим следующий пример, где вам нужно рассчитать возможные поездки для поездки на гастролях. Сначала мы генерируем возможные отключения:

    static IEnumerable<Trip> CreatePossibleTrips()
    {
        for (int i = 0; i < 1000000; i++)
        {
            yield return new Trip
            {
                Id = i.ToString(),
                Driver = new Driver { Id = i.ToString() }
            };
        }
    }

Затем повторите каждую поездку:

    static void Main(string[] args)
    {
        foreach (var trip in CreatePossibleTrips(trips))
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

Если вы используете List вместо yield, вам нужно будет выделить 1 миллион объектов в память (~ 190mb), и этот простой пример займет ~ 1400 мс. Однако, если вы используете выход, вам не нужно вставлять все эти временные объекты в память, и вы получите значительно более быструю скорость алгоритма: этот пример займет всего ~ 400 мс для работы без какого-либо потребления памяти.

  • 2
    под покровом что такое урожайность? Я бы подумал, что это список, следовательно, как это улучшит использование памяти?
  • 1
    @rolls yield работает под прикрытием, внедряя конечный автомат внутри. Вот SO ответ с 3 подробными сообщениями в блоге MSDN, которые объясняют реализацию в деталях. Автор Раймонд Чен @ MSFT
8

Это не что иное, как точка, но поскольку вопрос отмечен лучшими практиками, я пойду вперед и брошу свои два цента. Для этого типа я очень предпочитаю превращать его в свойство:

public static IEnumerable<Product> AllProducts
{
    get {
        using (AdventureWorksEntities db = new AdventureWorksEntities()) {
            var products = from product in db.Product
                           select product;

            return products;
        }
    }
}

Конечно, это немного больше котельной, но код, который использует это, будет выглядеть намного чище:

prices = Whatever.AllProducts.Select (product => product.price);

против

prices = Whatever.GetAllProducts().Select (product => product.price);

Примечание: Я бы не сделал этого для каких-либо методов, которые могут потребовать времени для выполнения своей работы.

8

Предполагая, что ваши продукты LINQ класс использует аналогичный доход для перечисления/итерации, первая версия более эффективна, поскольку она дает только одно значение при каждом повторении.

Второй пример - преобразование перечислителя/итератора в список с помощью метода ToList(). Это означает, что он вручную выполняет итерацию по всем элементам в перечислителе, а затем возвращает плоский список.

7

А как насчет этого?

public static IEnumerable<Product> GetAllProducts()
{
    using (AdventureWorksEntities db = new AdventureWorksEntities())
    {
        var products = from product in db.Product
                       select product;

        return products.ToList();
    }
}

Я думаю, это намного чище. Тем не менее, у меня нет VS2008, чтобы проверить. В любом случае, если Products реализует IEnumerable (как кажется - используется в инструкции foreach), я бы возвращал его напрямую.

  • 2
    Пожалуйста, измените OP, чтобы включить больше информации вместо публикации ответов.
  • 0
    Ну, вы должны сказать мне, что ОП означает именно :-) Спасибо
Показать ещё 1 комментарий
3

В этом случае я бы использовал версию 2 кода. Поскольку у вас есть полный список доступных продуктов и что ожидаемое "потребителем" этого метода вызовет, он должен будет отправить полную информацию обратно вызывающему абоненту.

Если вызывающему абоненту этого метода требуется "одна" информация одновременно, а потребление следующей информации - по требованию, тогда было бы полезно использовать возврат доходности, который будет гарантировать, что команда выполнения будет возвращена вызывающий, когда имеется единица информации.

Некоторые примеры, в которых можно использовать возврат доходности:

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

Чтобы ответить на ваши вопросы, я бы использовал версию 2.

2

Верните список напрямую. Преимущества:

  • Это более ясно.
  • Список можно использовать повторно. (итератор не является) на самом деле не истинно, спасибо Jon

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

  • 0
    Не забывайте, что он возвращается в IEnumerable <T>, а не в IEnumerator <T> - вы можете снова вызвать GetEnumerator.
  • 0
    Даже если вы заранее знаете, что весь список нужно будет рассчитать, все равно может быть полезно использовать доходность. Одним из примеров является, когда коллекция содержит сотни тысяч предметов.
1

Ключевая фраза возврата возврата используется для поддержки конечного автомата для конкретной коллекции. Везде, где CLR видит ключевую фразу yield return, CLR реализует шаблон Enumerator для этого фрагмента кода. Этот тип реализации помогает разработчику от всех типов сантехники, которые нам в противном случае пришлось бы делать в отсутствие ключевого слова.

Предположим, если разработчик фильтрует некоторую коллекцию, повторяя ее, а затем извлекая эти объекты в какой-то новой коллекции. Этот вид сантехники довольно монотонный.

Подробнее о ключевое слово здесь в этой статье.

0

Я не думаю, что кто-то упомянул об этом, и я надеюсь, что я прав, сказав это, но одна возможная проблема с версией 1 кода будет заключаться в том, что соединение с БД остается открытым до тех пор, пока вы не закончите итерацию через коллекцию.

-3

Использование yield похоже на ключевое слово return, за исключением того, что оно вернет генератор. И объект генератор будет проходить только один раз.

yield имеет два преимущества:

  • Вам не нужно дважды считывать эти значения;
  • Вы можете получить множество дочерних узлов, но не должны помещать их в память.

Существует еще одно ясное объяснение, возможно, поможет вам.

  • 4
    неправильный язык это C # не Python

Ещё вопросы

Сообщество Overcoder
Наверх
Меню