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

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

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

Версия 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>();
    }
}
3.01.2009 22:43:35
yieldпривязан IEnumerable<T>и к своему виду. Это в некотором роде ленивая оценка
Jaider 20.12.2012 22:29:39
Вот отличный ответ на аналогичный вопрос. stackoverflow.com/questions/15381708/…
Sanjeev Rai 13.06.2013 10:06:54
Вот хороший пример использования: stackoverflow.com/questions/3392612/…
ValGe 18.02.2014 16:48:50
Я вижу хороший пример использования, yield returnесли код, который перебирает результаты, GetAllProducts()дает пользователю возможность преждевременно отменить обработку.
JMD 27.03.2014 16:04:10
Я нашел эту тему действительно полезной: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski 11.09.2014 14:49:43
16 ОТВЕТОВ
РЕШЕНИЕ

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

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

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

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

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

803
3.01.2009 23:01:41
Я бы заметил, что в вашем примере в вопросе 3 объединены два преимущества. 1) Это распределяет вычислительные затраты (иногда выгода, иногда нет) 2) Это может лениво избегать вычислений бесконечно во многих случаях использования. Вы не упомянули о потенциальном недостатке, заключающемся в промежуточном состоянии. Если у вас есть значительное количество промежуточного состояния (скажем, HashSet для удаления дубликатов), то использование yield может привести к увеличению объема используемой памяти.
Kennet Belenky 11.09.2012 17:15:09
Кроме того, если каждый отдельный элемент очень большой, но к ним нужно обращаться только последовательно, доходность лучше.
Kennet Belenky 11.09.2012 17:26:17
И наконец ... есть немного странный, но иногда эффективный метод использования yield для написания асинхронного кода в очень сериализованной форме.
Kennet Belenky 11.09.2012 17:27:55
Другой пример, который может быть интересным, - это чтение довольно больших файлов CSV. Вы хотите прочитать каждый элемент, но вы также хотите извлечь свою зависимость. Выход, возвращающий IEnumerable <>, позволит вам возвращать каждую строку и обрабатывать каждую строку в отдельности. Нет необходимости читать 10 Мб файл в память. Всего одна строка за раз.
Maxime Rouiller 4.01.2013 13:46:25
Yield returnКажется, это сокращение для написания вашего собственного класса итератора (реализовать IEnumerator). Следовательно, упомянутые преимущества также применимы к пользовательским классам итераторов. Во всяком случае, обе конструкции сохраняют промежуточное состояние. В его самой простой форме речь идет об удержании ссылки на текущий объект.
J. Ouwehand 6.09.2016 10:37:32

И что по этому поводу?

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), я бы вернул его напрямую.

7
29.06.2012 06:14:27
Пожалуйста, измените OP, чтобы включить больше информации вместо публикации ответов.
Brian Rasmussen 3.01.2009 22:56:32
Ну, вы должны сказать мне, что ОП означает именно :-) Спасибо
petr k. 3.01.2009 23:01:16
Оригинальный пост, я полагаю. Я не могу редактировать сообщения, так что, похоже, так и есть.
petr k. 3.01.2009 23:02:06

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

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

8
3.01.2009 22:58:29

Верните список напрямую. Льготы:

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

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

3
3.01.2009 23:25:01
Не забывайте, что он возвращается в IEnumerable <T>, а не в IEnumerator <T> - вы можете снова вызвать GetEnumerator.
Jon Skeet 3.01.2009 23:11:18
Даже если вы заранее знаете, что весь список нужно будет рассчитать, все равно может быть полезно использовать доходность. Одним из примеров является, когда коллекция содержит сотни тысяч предметов.
Val 21.06.2017 15:10:25

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

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

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

12
3.01.2009 23:30:21

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

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
3.01.2009 23:30:25

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

27
4.01.2009 21:51:13
Презентация предоставляет простой обзор. Детали того, как это работает в C #, обсуждаются Рэем Ченом в ссылках на stackoverflow.com/a/39507/939250 . Первая ссылка подробно объясняет, что есть второй, неявный возврат в конце методов возврата доходности.
Donal Lafferty 16.09.2012 19:33:49

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

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

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

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; }
}

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

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

29
18.05.2011 04:36:41
Ваш пример кода является отличным примером того, как использовать yield OUTSIDE для блока for или foreach. Большинство примеров показывают доходность внутри итератора. Очень полезно, так как я только собирался задать вопрос о SO Как использовать yield вне итератора!
shelbypereira 15.08.2019 11:12:12
Мне никогда не приходило в голову использовать yieldтаким образом. Это похоже на элегантный способ эмулировать шаблон асинхронного ожидания / ожидания (который, я полагаю, будет использоваться вместо того, yieldчтобы переписать его сегодня). Считаете ли вы, что это творческое использование yieldпринесло (без каламбура) убывающую отдачу в течение многих лет по мере развития C # с тех пор, как вы ответили на этот вопрос? Или вы все еще придумываете модернизированные умные варианты использования, такие как эта? И если так, не могли бы вы поделиться еще одним интересным сценарием для нас?
Lopsided 5.03.2020 16:39:23

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

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

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

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

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

5
28.09.2016 14:04:54

В качестве концептуального примера для понимания того, когда вам следует использовать 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, потому что у вас уже есть заполненный список.

72
9.10.2012 21:45:30

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

637
2.06.2016 15:09:54
Я прекрасно понимаю, что этот ответ не является техническим ответом, но я считаю, что сходство между yield и потоковым видео служит хорошим примером при понимании ключевого слова yield. По этому поводу уже сказано все техническое, поэтому я попытался объяснить «другими словами». Существует ли правило сообщества, которое гласит, что вы не можете объяснить свои идеи в нетехнических терминах?
anar khalilov 8.07.2013 15:00:43
Я не уверен, кто за вас проголосовал или почему (я бы хотел, чтобы они это прокомментировали), но я думаю, что это в некоторой степени описывает это с нетехнической точки зрения.
senfo 8.07.2013 15:46:41
Все еще понимая концепцию, это помогло привести ее к дальнейшему вниманию, хорошая аналогия.
Tony 21.10.2014 13:28:46
Мне нравится этот ответ, но он не отвечает на вопрос.
ANeves 2.10.2015 14:56:13

У выхода есть два отличных применения

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

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

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

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

11
5.04.2014 18:48:34

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

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

    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!
}

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

10
27.05.2015 20:25:43
чтобы сократить двусмысленность, уточните, когда вы говорите, может, это будет или может быть? может ли первый возвратиться, а не выполнить второй выход?
Johno Crawford 7.02.2019 15:42:31
@JohnoCrawford второй оператор yield будет выполняться, только если перечислено второе / следующее значение IEnumerable. Вполне возможно, что он не будет, например, F().Any()- он вернется после попытки перечислить только первый результат. В общем, вы не должны полагаться на IEnumerable yieldизменение состояния программы, потому что оно может фактически не
Zac Faragher 11.07.2019 23:49:45

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

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

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

1
19.07.2016 20:44:32

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

    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())
        {
            // possible trip is actually calculated only at this point, because of yield
            if (IsTripGood(trip))
            {
                // match good trip
            }
        }
    }

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

17
6.01.2020 10:37:37
под покровом что такое урожайность? Я бы подумал, что это список, следовательно, как это улучшит использование памяти?
rolls 7.05.2017 02:52:34
@rolls yieldработает под прикрытием, внедряя конечный автомат внутри. Вот SO ответ с 3 подробными сообщениями в блоге MSDN, которые объясняют реализацию очень подробно. Автор Раймонд Чен @ MSFT
Shiva 23.02.2018 21:49:39

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

У выхода есть два преимущества:

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

Есть еще одно четкое объяснение, может быть, вам поможет.

-4
4.04.2017 02:28:02