Правильное использование "доходности доходности"
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>();
}
}
Ответы
Ответ 1
Я обычно использую return-return при вычислении следующего элемента в списке (или даже следующей группы элементов).
Используя вашу версию 2, перед возвратом у вас должен быть полный список.
Используя return-return, вам действительно нужно иметь следующий элемент перед возвратом.
Помимо всего прочего, это помогает распределить вычислительную стоимость сложных вычислений на более высокий временной интервал. Например, если список подключен к графическому интерфейсу и пользователь никогда не переходит на последнюю страницу, вы никогда не вычисляете конечные элементы в списке.
Другой случай, когда yield-return предпочтительнее, - это если IEnumerable представляет бесконечный набор. Рассмотрим список простых чисел или бесконечный список случайных чисел. Вы никогда не сможете вернуть полный IEnumerable сразу, поэтому вы используете return-return, чтобы возвращать список пошагово.
В вашем конкретном примере у вас есть полный список продуктов, поэтому я бы использовал версию 2.
Ответ 2
Заполнение временного списка похоже на загрузку всего видео, тогда как использование yield
похоже на потоковое видео.
Ответ 3
В качестве концептуального примера для понимания, когда вы должны использовать 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, потому что у вас уже есть заполненный список.
Ответ 4
Это будет похоже на странное предложение, но я узнал, как использовать ключевое слово yield
в С#, прочитав презентацию о генераторах в Python: David M. Beazley http://www.dabeaz.com/generators/Generators.pdf. Вам не нужно много знать Python, чтобы понять презентацию - я этого не сделал. Мне было очень полезно объяснить не только то, как работают генераторы, но и почему вы должны заботиться.
Ответ 5
Я знаю, что это старый вопрос, но я хотел бы предложить один пример того, как ключевое слово 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; }
}
Это может помочь настроить что-то подобное и выполнить выполнение, чтобы посмотреть, что происходит.
Надеюсь, это поможет кому-то! Мне очень понравилось исследовать различные способы использования урожая.
Ответ 6
Две части кода действительно делают две разные вещи. Первая версия будет тянуть участников по мере необходимости. Вторая версия будет загружать все результаты в память, прежде чем вы начнете что-либо делать с ней.
Нет правильного или неправильного ответа на этот вопрос. Какой из них предпочтительнее, просто зависит от ситуации. Например, если у вас есть время, необходимое для завершения запроса, и вам нужно сделать что-то полузасушливое с результатами, вторая версия может быть предпочтительной. Но будьте осторожны с большими наборами результатов, особенно если вы используете этот код в 32-битном режиме. Несколько раз я был укушен исключениями OutOfMemory при выполнении этого метода.
Главное, что нужно помнить, это: различия в эффективности. Таким образом, вы, вероятно, должны пойти с тем, что делает ваш код более простым и изменить его только после профилирования.
Ответ 7
Выход имеет два больших использования
Это помогает обеспечить пользовательскую итерацию без создания временных наборов. (загрузка всех данных и цикл)
Это помогает делать итерацию с сохранением состояния. (потоковая передача)
Ниже приведено простое видео, которое я создал с полной демонстрацией, чтобы поддержать вышеупомянутые две точки.
http://www.youtube.com/watch?v=4fju3xcm21M
Ответ 8
Вот что 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!
}
В этих случаях, помня, что возврат доходности не является "окончательным", как возвращение полезно.
Ответ 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 мс для работы без какого-либо потребления памяти.
Ответ 10
Предполагая, что ваши продукты LINQ класс использует аналогичный доход для перечисления/итерации, первая версия более эффективна, поскольку она дает только одно значение при каждом повторении.
Второй пример - преобразование перечислителя/итератора в список с помощью метода ToList(). Это означает, что он вручную выполняет итерацию по всем элементам в перечислителе, а затем возвращает плоский список.
Ответ 11
Это не что иное, как точка, но поскольку вопрос отмечен лучшими практиками, я пойду вперед и брошу свои два цента. Для этого типа я очень предпочитаю превращать его в свойство:
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);
Примечание: Я бы не сделал этого для каких-либо методов, которые могут потребовать времени для выполнения своей работы.
Ответ 12
А как насчет этого?
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), я бы возвращал его напрямую.
Ответ 13
В этом случае я бы использовал версию 2 кода. Поскольку у вас есть полный список доступных продуктов и что ожидаемое "потребителем" этого метода вызовет, он должен будет отправить полную информацию обратно вызывающему абоненту.
Если вызывающему абоненту этого метода требуется "одна" информация одновременно, а потребление следующей информации - по требованию, тогда было бы полезно использовать возврат доходности, который будет гарантировать, что команда выполнения будет возвращена вызывающий, когда имеется единица информации.
Некоторые примеры, в которых можно использовать возврат доходности:
- Комплексный, пошаговый расчет, когда вызывающий абонент ждет данных по шагу за раз
- Пейджинг в графическом интерфейсе - где пользователь может никогда не дойти до последней страницы, и для отображения на текущей странице требуется только подмножество информации.
Чтобы ответить на ваши вопросы, я бы использовал версию 2.
Ответ 14
Верните список напрямую. Преимущества:
- Это более ясно.
-
Список можно использовать повторно. (итератор не является) на самом деле не истинно, спасибо Jon
Вы должны использовать итератор (доход), если считаете, что вам, вероятно, не придется перебирать все пути до конца списка или когда у него нет конца. Например, клиентский вызов будет искать первый продукт, который удовлетворяет некоторым предикатам, вы можете подумать об использовании итератора, хотя это надуманный пример, и, вероятно, есть лучшие способы его достижения. В принципе, если вы заранее знаете, что весь список нужно будет рассчитать, просто сделайте это заранее. Если вы считаете, что этого не произойдет, подумайте об использовании версии итератора.
Ответ 15
Ключевая фраза возврата возврата используется для поддержки конечного автомата для конкретной коллекции. Везде, где CLR видит ключевую фразу yield return, CLR реализует шаблон Enumerator для этого фрагмента кода. Этот тип реализации помогает разработчику от всех типов сантехники, которые нам в противном случае пришлось бы делать в отсутствие ключевого слова.
Предположим, если разработчик фильтрует некоторую коллекцию, повторяя ее, а затем извлекая эти объекты в какой-то новой коллекции. Этот вид сантехники довольно монотонный.
Подробнее о ключевое слово здесь в этой статье.
Ответ 16
Я не думаю, что кто-то упомянул об этом, и я надеюсь, что я прав, сказав это, но одна возможная проблема с версией 1 кода будет заключаться в том, что соединение с БД остается открытым до тех пор, пока вы не закончите итерацию через коллекцию.
Ответ 17
Использование yield похоже на ключевое слово return, за исключением того, что оно вернет генератор. И объект генератор будет проходить только один раз.
yield имеет два преимущества:
- Вам не нужно дважды считывать эти значения;
- Вы можете получить множество дочерних узлов, но не должны помещать их в память.
Существует еще одно ясное объяснение, возможно, поможет вам.