Правильне використання "прибутку"


903

Вихід ключового слова є одним з тих ключових слів в 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>();
    }
}

38
yieldприв’язаний до IEnumerable<T>і його виду. Це дещо ледача оцінка
Джайдер

ось точна відповідь на подібне запитання. stackoverflow.com/questions/15381708/…
Саньєєв Рай

1
Ось хороший приклад використання: stackoverflow.com/questions/3392612 / ...
Valge

6
Я вважаю хорошим випадком використання, yield returnякщо код, який повторюється за результатами, GetAllProducts()дозволяє користувачеві передчасно скасувати обробку.
JMD

2
Я вважаю цю тему дуже корисною: programmers.stackexchange.com/a/97350/148944
PiotrWolkowski,

Відповіді:


806

Я схильний використовувати дохідність, коли я обчислюю наступний елемент у списку (або навіть наступну групу елементів).

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

Крім усього іншого, це допомагає поширити обчислювальну вартість складних обчислень на більші часові рамки. Наприклад, якщо список підключений до графічного інтерфейсу і користувач ніколи не переходить на останню сторінку, ви ніколи не обчислюєте остаточні елементи у списку.

Інший випадок, коли віддача від переваги є кращою, якщо IEnumerable являє собою нескінченний набір. Розглянемо список простих чисел або нескінченний список випадкових чисел. Ніколи не можна повернути повний IEnumerable відразу, тому ви використовуєте return-return, щоб повертати список поступово.

У вашому конкретному прикладі ви маєте повний перелік продуктів, тому я використовую версію 2.


31
Я б зрозумів, що у вашому прикладі в питанні 3 суперечать дві переваги. 1) Він розподіляє обчислювальну вартість (іноді вигода, іноді ні) 2) Це може ліниво уникати обчислень на невизначений час у багатьох випадках використання. Ви не можете згадати потенційний недолік, який він зберігає навколо проміжного стану. Якщо у вас є значне кількість проміжного стану (скажімо, HashSet для усунення дублікатів), то використання виходу може збільшити слід вашої пам'яті.
Кеннет Беленький

8
Крім того, якщо кожен окремий елемент дуже великий, але доступ до них потрібно лише послідовно, вихід кращий.
Кеннет Беленький

2
І нарешті ... є дещо вибаглива, але час від часу ефективна методика використання уроку для запису асинхронного коду у дуже серіалізованій формі.
Кеннет Беленький

12
Інший приклад, який може бути цікавим, - це читання досить великих файлів CSV. Ви хочете прочитати кожен елемент, але ви також хочете витягнути залежність. Вихід, що повертає IEnumerable <>, дозволить повернути кожен рядок та обробити кожен рядок окремо. Не потрібно читати 10 Мб файл в пам'ять. Лише по одному рядку.
Максим Руйлер

1
Yield returnЗдається, це скорочення для написання власного користувальницького класу ітераторів (реалізувати IEnumerator). Отже, згадані переваги стосуються також і спеціальних класів ітераторів. У будь-якому випадку, обидві конструкції зберігають проміжний стан. У своїй найпростішій формі мова йде про те, щоб мати посилання на поточний об'єкт.
J. Ouwehand

641

Створення тимчасового списку - це як завантаження всього відео, тоді як використання yield- це як потокове передавання цього відео.


180
Я прекрасно знаю, що ця відповідь не є технічною відповіддю, але я вважаю, що схожість між урожайністю та потоковою передачею відео слугує хорошим прикладом при розумінні ключового слова дохідності. Про це вже було сказано все технічне, тому я спробував пояснити «іншими словами». Чи існує правило спільноти, яке говорить про те, що ви не можете пояснити свої ідеї нетехнічними термінами?
анар халілов

13
Я не впевнений, хто вас зголосив або чому (я б хотів, щоб вони прокоментували), але я думаю, що це дещо описує це з нетехнічної перспективи.
senfo

22
Все-таки осмисливши концепцію, і це допомогло привести її до фокусу, приємної аналогії.
Тоні

11
Мені подобається ця відповідь, але вона не відповідає на питання.
ANeves

73

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


30

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

Примітка. Не думайте про ключове слово "урожайність" як про ще один спосіб створення колекції. Велика частина потужності прибутковості полягає в тому, що виконання призупинено у вашому методі чи властивості, поки викликовий код не повториться над наступним значенням. Ось мій приклад:

Використання ключового слова прибутковості (поряд із впровадженням процедур Roli Eisenburg 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, зателефонувати за методом входу на веб-службі, встановити прапор IsLoggedIn на значення повернення, а потім відключити BusyIndicator.

Ось як це працює: IResult має метод Execute та подія завершено. Caliburn.Micro захоплює IEnumerator з виклику HandleButtonClick () і передає його методу Coroutine.BeginExecute. Метод BeginExecute починає ітерацію через IResults. Після повернення першого IResult виконання призупиняється всередині HandleButtonClick (), а BeginExecute () додає обробник подій до події завершено і викликає Execute (). IResult.Execute () може виконувати або синхронну, або асинхронну задачу, і запускає подія завершено, коли це зроблено.

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

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

Сподіваюся, це допоможе комусь вийти! Мені дуже подобалося вивчати різні способи використання врожаю.


1
зразок вашого коду - чудовий приклад того, як використовувати вихід НАЗВИ блоку for або foreach. Більшість прикладів показують прибуток урожайності в ітераторі. Дуже корисно, оскільки я ось-ось збирався задати питання про SO Як використовувати урожай поза ітератором!
shelbypereira

Мені ніколи не спадало на думку використовувати yieldцей спосіб. Це здається елегантним способом емуляції шаблону асинхрон / очікування (який, я вважаю, використовувався б замість цього, yieldякби це було переписано сьогодні). Чи виявили ви, що ці творчі використання yieldприносили (не каламбур) зменшуючи прибутки протягом багатьох років, оскільки C # еволюціонував з того часу, як ви відповіли на це запитання? Або ви все ще придумуєте модернізовані розумні випадки використання, такі як ця? І якщо так, чи не проти ви поділитися ще одним цікавим для нас сценарієм?
Лопсайдер

27

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


1
Презентація забезпечує простий огляд. Подробиці того, як це працює в C #, обговорюється Реєм Чен у посиланнях на stackoverflow.com/a/39507/939250 Перша посилання детально пояснює, що в кінці методів повернення врожаю є друге неявне повернення.
Donal Lafferty

18

Повернення виходу може бути дуже потужним для алгоритмів, де потрібно повторити мільйони об'єктів. Розглянемо наступний приклад, де потрібно обчислити можливі поїздки для спільного використання їзди. Спочатку ми генеруємо можливі поїздки:

    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 замість урожайності, вам потрібно буде виділити 1 мільйон об’єктів у пам'яті (~ 190mb), і цей простий приклад запустить ~ 1400ms. Однак якщо ви використовуєте урожайність, вам не потрібно зберігати всі ці тимчасові об’єкти в пам’яті, і ви отримаєте значно більшу швидкість алгоритму: цей приклад займе лише ~ 400 мс для запуску без споживання пам'яті взагалі.


2
під покривами що таке врожайність? Я би подумав, що це список, отже, як він покращить використання пам'яті?
котиться

1
@rolls yieldпрацює під кришками, внутрішньо впроваджуючи державну машину. Ось відповідь ТА з 3 детальними публікаціями блогу MSDN, які пояснюють реалізацію дуже докладно. Автор Раймонд Чен @ MSFT
Шива

13

Два фрагменти коду справді роблять дві різні речі. Перша версія підтягне членів, скільки вам потрібно. Друга версія завантажить усі результати в пам’ять, перш ніж ви почнете з цим робити щось.

На цю відповідь немає правильної чи неправильної відповіді. Хто з них кращий, просто залежить від ситуації. Наприклад, якщо у вас є обмежений час, що вам потрібно виконати запит, і вам потрібно зробити щось напівскладне з результатами, друга версія може бути кращою. Але остерігайтеся великих наборів результатів, особливо якщо ви використовуєте цей код у 32-бітному режимі. Мене кілька разів покусали винятки OutOfMemory при виконанні цього методу.

Ключове, що слід пам’ятати, це ось що: відмінності полягають у ефективності. Таким чином, ви, ймовірно, повинні йти з тим, що робить ваш код простішим та змінювати його лише після профілювання.


11

Вихід має два чудові можливості

Це допомагає забезпечити власну ітерацію із створення тимчасових колекцій. (завантаження всіх даних і циклічне використання)

Це допомагає зробити державну ітерацію. (потокове)

Нижче наведено просте відео, яке я створив з повною демонстрацією, щоб підтримати вищевказані два моменти

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


10

Це те, що Кріс Селлс розповідає про ці твердження в мові програмування C # ;

Я іноді забуваю, що прибуток прибутковості не такий, як повернення, оскільки код після повернення доходу може бути виконаний. Наприклад, код після першого повернення сюди ніколи не може бути виконаний:

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

У цих випадках запам’ятати, що прибуток не є “остаточним”, як повернення.


щоб зменшити неоднозначність, будь ласка, уточнюйте, коли ви кажете, може, це, буде чи може? може бути можливим, щоб перший повернувся, а другий не був виконаний?
Джонно Кроуфорд

@JohnoCrawford, друге твердження про вихід буде виконано лише в тому випадку, якщо перераховується друге / наступне значення IEnumerable. Цілком можливо, що це не буде, наприклад F().Any()- це повернеться, намагаючись перерахувати лише перший результат. Взагалі, вам не слід покладатися на IEnumerable yieldзміну стану програми, тому що вона може насправді не спрацьовувати
Зак Фарагер

8

Припускаючи, що ваші продукти клас LINQ використовує аналогічний вихід для перерахування / повторення, перша версія є більш ефективною, оскільки вона дає лише одне значення кожного разу, коли вона повторюється.

Другий приклад - перетворення нумератора / ітератора до списку методом ToList (). Це означає, що він вручну переробляє всі елементи в перелік, а потім повертає плоский список.


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

Примітка. Я б не робив цього для жодних методів, які можуть зайняти деякий час для їхньої роботи.


7

А що з цим?

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

        return products.ToList();
    }
}

Я думаю, це набагато чистіше. У мене немає VS2008 під рукою, щоб перевірити. У будь-якому випадку, якщо Продукти реалізують IEnumerable (як здається - він використовується у заяві foreach), я б повернув його безпосередньо.


2
Редагуйте ОП, щоб включити більше інформації замість публікації відповідей.
Брайан Расмуссен

Що ж, ви повинні сказати мені, на що саме стоїть ОП :-) Спасибі
Петре к.

Оригінальний пост, я припускаю. Я не можу редагувати публікації, тому, здавалося, це був шлях.
Петр к.

5

Я б використовував версію 2 коду в цьому випадку. Оскільки у вас є повний перелік продуктів, і це саме те, чого очікує «споживач» цього дзвінка, вам потрібно буде відправити повну інформацію назад абоненту.

Якщо абонент цього методу вимагає "однієї" інформації одночасно, а споживання наступної інформації базується на вимогу, то було б вигідно використовувати віддачу, яка гарантує, що команда виконання буде повернена абоненту, коли одиниця інформації доступна.

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

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

Щоб відповісти на ваші запитання, я б використав версію 2.


3

Поверніть список безпосередньо. Переваги:

  • Це більш зрозуміло
  • Список багаторазовий. (ітератор не є) насправді не так, дякую Джон

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


Не забувайте, що він повертається в IEnumerable <T>, а не IEnumerator <T> - ви можете зателефонувати GetEnumerator знову.
Джон Скіт

Навіть якщо ви знаєте заздалегідь, весь список потрібно буде обчислити, можливо, все-таки вигідно використовувати прибуток. Один із прикладів - коли колекція містить сотні тисяч предметів.
Вал

1

Ключова фраза повернення прибутковості використовується для підтримки стану машини для певної колекції. Де б CLR не бачив використовувану ключову фразу повернення прибутків, CLR реалізує шаблон Enumerator до цього фрагмента коду. Цей тип реалізації допомагає розробникові з усіх типів сантехніки, які ми мали би в іншому випадку робити, якщо б не було ключового слова.

Припустимо, якщо розробник фільтрує якусь колекцію, повторюючи колекцію, а потім витягуючи ці об'єкти в новій колекції. Цей вид сантехніки досить одноманітний.

Більше про ключове слово тут, у цій статті .


-4

Використання урожайності схоже на ключове слово return , за винятком того, що воно поверне генератор . А об’єкт генератора пройде лише один раз .

урожай має дві переваги:

  1. Вам не потрібно читати ці значення двічі;
  2. Ви можете отримати багато дочірніх вузлів, але не потрібно залишати їх у пам’яті.

Є ще одне чітке пояснення, можливо, допоможе вам.

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