Для чого використовується ключове слово дохідності в C #?


828

У питанні Як я можу розкрити лише фрагмент IList <> питання в одному з відповідей був такий фрагмент коду:

IEnumerable<object> FilteredList()
{
    foreach(object item in FullList)
    {
        if(IsItemInPartialList(item))
            yield return item;
    }
}

Що там робить ключове слово? Я бачив, як це посилалося в декількох місцях, і ще одне питання, але я не зовсім зрозумів, що це насправді. Я звик думати про вихід у сенсі, коли одна нитка поступається іншій, але це не здається актуальним.


Просто посилання MSDN про це знаходиться тут msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx
Розробник

14
Це не дивно. Плутанина виникає з того, що ми зумовлені бачити "повернення" як функцію виводу, тоді як передує "вихід", це не так.
Ларрі

4
Я читав документи, але боюся, що досі не розумію :(
Ortund

Відповіді:


737

yieldКлючові слова на насправді роблять досить багато тут.

Функція повертає об'єкт, який реалізує IEnumerable<object>інтерфейс. Якщо функція виклику починається foreachнад цим об'єктом, функція викликається ще раз, поки вона не «поступається». Це синтаксичний цукор, введений в C # 2.0 . У попередніх версіях вам довелося створити свій власний IEnumerableіIEnumerator об’єкти, щоб робити такі речі.

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

public void Consumer()
{
    foreach(int i in Integers())
    {
        Console.WriteLine(i.ToString());
    }
}

public IEnumerable<int> Integers()
{
    yield return 1;
    yield return 2;
    yield return 4;
    yield return 8;
    yield return 16;
    yield return 16777216;
}

Переглядаючи приклад, ви знайдете перший дзвінок, який Integers()повертається 1. Другий дзвінок повертається, 2і лінія yield return 1не виконується знову.

Ось приклад із реального життя:

public IEnumerable<T> Read<T>(string sql, Func<IDataReader, T> make, params object[] parms)
{
    using (var connection = CreateConnection())
    {
        using (var command = CreateCommand(CommandType.Text, sql, connection, parms))
        {
            command.CommandTimeout = dataBaseSettings.ReadCommandTimeout;
            using (var reader = command.ExecuteReader())
            {
                while (reader.Read())
                {
                    yield return make(reader);
                }
            }
        }
    }
}

113
У цьому випадку це буде простіше, я просто використовую тут ціле число, щоб показати, як працює прибуток. Приємні речі щодо використання віддачі в тому, що це дуже швидкий спосіб реалізації схеми ітератора, тому речі оцінюються ліниво.
Мендельте

111
Також варто зазначити, що ви можете користуватися, yield break;коли більше не хочете повертати більше предметів.
Рорі

7
yieldне є ключовим словом. Якби це тоді, я не міг би використати урожай як ідентифікатор, як уint yield = 500;
Брандін,

5
@Brandin тому, що всі мови програмування підтримують два типи ключових слів, а саме зарезервоване та контекстне. прибутковість потрапляє в пізнішу категорію, тому компілятор C # забороняє ваш код. Більше інформації тут: ericlippert.com/2009/05/11/reserved-and-contextual-keywords Ви були б із задоволенням знаєте, що є також зарезервовані слова, які не розпізнаються мовою як ключові слова. Наприклад, наприклад, goto in java. Більше інформації тут: stackoverflow.com/questions/2545103/…
RBT

7
'If a calling function starts foreach-ing over this object the function is called again until it "yields"'. мені не звучить правильно. Я завжди думав про ключове слово c # урожай у контексті "врожай дає багатший урожай", а не "машина поступається пішоходу".
Зак

369

Ітерація. Він створює державну машину "під кришками", яка запам'ятовує, де ви були на кожному додатковому циклі функції, і вибирає звідти.


210

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

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

  2. Це допомагає зробити державну ітерацію. введіть тут опис зображення

Щоб пояснити вище два моменти більш наочно, я створив просте відео, яке ви можете подивитися тут


13
Відео допоможе мені чітко зрозуміти yield. Стаття щодо проекту коду ShivprasadKoirala У чому полягає використання урожаю C #? того ж пояснення є і хорошим джерелом
Dush

Я також додав би як третій момент, що yieldє "швидким" способом створення користувацького IEnumerator (швидше, щоб клас реалізував інтерфейс IEnumerator).
MrTourkos

Я переглянув ваше відео Shivprasad, і це чітко пояснило використання ключового слова дохідності.
Tore Aurstad

Дякуємо за відео !. Дуже добре пояснено!
Roblogic

Відмінне відео, але цікаво ... Реалізація, що використовує урожай, очевидно, є більш чистою, але вона, по суті, повинна створювати власну тимчасову пам'ять або / та Список внутрішньо, щоб слідкувати за станом (а точніше створити машину стану). Отже, чи "Yield" робить щось інше, ніж спрощення впровадження та полегшення речей, чи є щось інше? Як щодо ефективності, чи працює запуск коду з використанням Yield більш-менш ефективно / швидко, ніж без?
жорсткі запитання

135

Нещодавно Реймонд Чен також випустив цікаву серію статей про ключове слово.

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


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

3
частина 1 пояснює синтаксичний цукор "прибутку". відмінне пояснення!
Dror Weiss

99

На перший погляд, прибутковість - це. NET цукор для повернення IEnumerable .

Без виходу всі елементи колекції створюються одразу:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        return new List<SomeData> {
            new SomeData(), 
            new SomeData(), 
            new SomeData()
        };
    }
}

Той самий код, використовуючи дохідність, повертає пункт за пунктом:

class SomeData
{
    public SomeData() { }

    static public IEnumerable<SomeData> CreateSomeDatas()
    {
        yield return new SomeData();
        yield return new SomeData();
        yield return new SomeData();
    }
}

Перевага використання урожайності полягає в тому, що якщо функції, що споживає ваші дані, просто потрібен перший елемент колекції, решта елементів не буде створена.

Оператор прибутковості дозволяє створювати елементи так, як цього вимагають. Це вагомий привід використовувати його.


40

yield returnвикористовується з обчислювачами. Під час кожного виклику заяви про вихід, контроль повертається абоненту, але він забезпечує збереження стану виклику. Завдяки цьому, коли абонент перераховує наступний елемент, він продовжує виконання методу callee з оператора негайно післяyield оператора.

Спробуємо зрозуміти це на прикладі. У цьому прикладі, відповідно до кожного рядка, я згадав порядок протікання виконання.

static void Main(string[] args)
{
    foreach (int fib in Fibs(6))//1, 5
    {
        Console.WriteLine(fib + " ");//4, 10
    }            
}

static IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 0, currFib = 1; i < fibCount; i++)//2
    {
        yield return prevFib;//3, 9
        int newFib = prevFib + currFib;//6
        prevFib = currFib;//7
        currFib = newFib;//8
    }
}

Також стан підтримується для кожного перерахунку. Припустимо, у мене є ще один виклик Fibs()методу, тоді стан буде скинутий для нього.


2
встановити prevFib = 1 - перше число Фібоначчі - це "1", а не "0"
fubo

31

Інтуїтивно, ключове слово повертає значення функції, не залишаючи її, тобто у прикладі вашого коду воно повертає поточне itemзначення, а потім відновлює цикл. Більш офіційно він використовується компілятором для створення коду для ітератора . Ітератори - це функції, які повертають IEnumerableоб'єкти. MSDN є кілька статей про них.


4
Ну, якщо бути точним, він не поновлює цикл, він призупиняє його, поки батьків не викликає "iterator.next ()".
Олексій

8
@jitbit Тому я використовував "інтуїтивно" та "більш формально".
Конрад Рудольф

31

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

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

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

  • Масштабованість, надійність та передбачуваність , ймовірно, поліпшаться, оскільки кількість записів істотно не впливає на потреби в ресурсах програми.
  • Продуктивність та чуйність , ймовірно, покращаться, оскільки обробка може розпочатися негайно, а не чекати, коли перша колекція буде завантажена спочатку.
  • Відновлення та використання , ймовірно, покращаться, оскільки додаток можна зупинити, запустити, перервати або відмовити. Лише ті елементи, які виконуються, будуть втрачені порівняно з попередньою отриманням усіх даних, де фактично використовувалася лише частина результатів.
  • Безперервна обробка можлива в середовищах, де додаються постійні потоки навантаження.

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

Приклад списку

    public class ContactListStore : IStore<ContactModel>
    {
        public IEnumerable<ContactModel> GetEnumerator()
        {
            var contacts = new List<ContactModel>();
            Console.WriteLine("ContactListStore: Creating contact 1");
            contacts.Add(new ContactModel() { FirstName = "Bob", LastName = "Blue" });
            Console.WriteLine("ContactListStore: Creating contact 2");
            contacts.Add(new ContactModel() { FirstName = "Jim", LastName = "Green" });
            Console.WriteLine("ContactListStore: Creating contact 3");
            contacts.Add(new ContactModel() { FirstName = "Susan", LastName = "Orange" });
            return contacts;
        }
    }

    static void Main(string[] args)
    {
        var store = new ContactListStore();
        var contacts = store.GetEnumerator();

        Console.WriteLine("Ready to iterate through the collection.");
        Console.ReadLine();
    }

Вихід з консолі
ContactListStore: Створення контакту 1
ContactListStore: Створення контакту 2
ContactListStore: Створення контакту 3
Готовий до перегляду колекції.

Примітка: вся колекція була завантажена в пам'ять, навіть не вимагаючи жодного елемента в списку

Приклад виходу

public class ContactYieldStore : IStore<ContactModel>
{
    public IEnumerable<ContactModel> GetEnumerator()
    {
        Console.WriteLine("ContactYieldStore: Creating contact 1");
        yield return new ContactModel() { FirstName = "Bob", LastName = "Blue" };
        Console.WriteLine("ContactYieldStore: Creating contact 2");
        yield return new ContactModel() { FirstName = "Jim", LastName = "Green" };
        Console.WriteLine("ContactYieldStore: Creating contact 3");
        yield return new ContactModel() { FirstName = "Susan", LastName = "Orange" };
    }
}

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();

    Console.WriteLine("Ready to iterate through the collection.");
    Console.ReadLine();
}

Вихід консолі
готовий до перегляду колекції.

Примітка. Колекція взагалі не виконувалася. Це пояснюється характером "відкладеного виконання" IEnumerable. Конструювання елемента відбуватиметься лише тоді, коли це дійсно потрібно.

Давайте назвемо колекцію ще раз і відзначимо поведінку, коли ми отримаємо перший контакт у колекції.

static void Main(string[] args)
{
    var store = new ContactYieldStore();
    var contacts = store.GetEnumerator();
    Console.WriteLine("Ready to iterate through the collection");
    Console.WriteLine("Hello {0}", contacts.First().FirstName);
    Console.ReadLine();
}

Вихід консолі
Готовий до
повтореннячерез колекцію ContactYieldStore: Створення контакту 1
Привіт Боб

Приємно! Лише перший контакт був сконструйований, коли клієнт "витягнув" товар із колекції.


1
Ця відповідь потребує більшої уваги! Thx
leon22

@ Leon22 абсолютно +2
ЗСШ

26

Ось простий спосіб зрозуміти концепцію: Основна ідея - якщо ви хочете колекцію, яку ви можете використовувати "foreach " , але збирати елементи в колекцію чомусь дорого (наприклад, запит їх із бази даних), І вам часто не знадобиться вся колекція, тоді ви створюєте функцію, яка будує колекцію по одному предмету і повертає його споживачеві (який потім може достроково припинити колекціонування).

Подумайте так: Ви йдете до м'ясної прилавки і хочете придбати півкіло нарізаної шинки. Різник бере 10-кілограмову шинку до спини, кладе її на машинку для нарізки, нарізає всю річ, потім повертає до вас купу скибочок і відміряє їй півкіло. (СТАРИЙ спосіб). З yield, м'ясник підводить машинку для нарізки до прилавку, і починає нарізати та "давати" кожен шматочок на шкалі, поки він не відміряє 1 фунт, потім загортає його для вас і ви готові. Старий Шлях може бути кращим для м'ясника (дозволяє йому організовувати свою техніку так, як йому подобається), але Новий Шлях явно ефективніший у більшості випадків для споживача.


18

yieldКлючові слова дозволяють створити IEnumerable<T>в формах на блоці ітератора . Цей блок ітераторів підтримує відкладене виконання, і якщо ви не знайомі з концепцією, це може здатися майже магічним. Однак наприкінці дня це просто код, який виконується без будь-яких дивних хитрощів.

Ітераторний блок можна описати як синтаксичний цукор, де компілятор генерує державну машину, яка відслідковує, наскільки прогресувало перерахування. Для перерахування нумерації ви часто використовуєте foreachцикл. Однак foreachпетля - це також синтаксичний цукор. Отже, ви маєте дві абстракції, вилучені з реального коду, тому спочатку може бути важко зрозуміти, як це все працює разом.

Припустимо, що у вас дуже простий блок ітератора:

IEnumerable<int> IteratorBlock()
{
    Console.WriteLine("Begin");
    yield return 1;
    Console.WriteLine("After 1");
    yield return 2;
    Console.WriteLine("After 2");
    yield return 42;
    Console.WriteLine("End");
}

У справжніх блоках ітераторів часто є умови та петлі, але коли ви перевіряєте умови та розгортаєте цикли, вони все одно закінчуються як yield заяви, переплетені з іншим кодом.

Для перерахування блоку ітератора використовується foreachцикл:

foreach (var i in IteratorBlock())
    Console.WriteLine(i);

Ось результат (сюрпризів тут немає):

Почніть
1
Після 1
2
Через 2
42
Кінець

Як зазначено вище, foreachце синтаксичний цукор:

IEnumerator<int> enumerator = null;
try
{
    enumerator = IteratorBlock().GetEnumerator();
    while (enumerator.MoveNext())
    {
        var i = enumerator.Current;
        Console.WriteLine(i);
    }
}
finally
{
    enumerator?.Dispose();
}

Намагаючись розплутати це, я створив діаграму послідовностей із видаленими абстракціями:

Схематична схема послідовності блоків C # ітераторів

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

Кожен раз, коли ви викликаєте свій блок ітераторів, створюється новий екземпляр державної машини. Однак жоден ваш код у блоці ітератора не виконується до enumerator.MoveNext()першого виконання. Ось як працює відкладене виконання. Ось (досить дурний) приклад:

var evenNumbers = IteratorBlock().Where(i => i%2 == 0);

На даний момент ітератор не виконується. WhereПоложення створює новий , IEnumerable<T>який обертає IEnumerable<T>повертається IteratorBlockале перелічуваних до сих пір не перераховані. Це відбувається під час виконання foreachциклу:

foreach (var evenNumber in evenNumbers)
    Console.WriteLine(eventNumber);

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

Зверніть увагу , що методи LINQ подобається ToList(), ToArray(), First(), і Count()т.д. буде використовувати foreachцикл для перерахування перелічуваних. Наприклад, ToList()буде перераховано всі елементи перелічуваного і збереже їх у списку. Тепер ви можете отримати доступ до списку, щоб отримати всі елементи перелічуваного без повторного виконання блоку ітераторів. Існує компроміс між використанням процесора для отримання елементів перелічених кількох разів і пам'яттю для зберігання елементів перерахунку для доступу до них кілька разів при використанні таких методів ToList().


18

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

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

просто та блискуче
Гаррі

10

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

У JavaScript те саме поняття називається Генераторами.


Найкраще пояснення поки. Це теж однакові генератори в python?
petrosmm

7

Це дуже простий і простий спосіб створити безліч для вашого об'єкта. Компілятор створює клас, який обгортає ваш метод і реалізує в цьому випадку IEnumerable <object>. Без ключового слова дохідності вам доведеться створити об’єкт, який реалізує IEnumerable <object>.


5

Це створює незліченну послідовність. Насправді це створює локальну послідовність IEnumerable та повертає її як результат методу


3

Це посилання має простий приклад

Навіть простіші приклади тут

public static IEnumerable<int> testYieldb()
{
    for(int i=0;i<3;i++) yield return 4;
}

Зауважте, що прибутковість прибутку не повернеться із методу. Можна навіть поставитиWriteLine післяyield return

Вищезазначене дає IE чисельність 4 ints 4,4,4,4

Тут з a WriteLine. Додамо 4 до списку, надрукуємо abc, потім додамо 4 до списку, потім завершимо метод і так дійсно повернемося з методу (як тільки метод завершиться, як це станеться з процедурою без повернення). Але це буде мати значення, IEnumerableсписок ints, який він повертає після завершення.

public static IEnumerable<int> testYieldb()
{
    yield return 4;
    console.WriteLine("abc");
    yield return 4;
}

Зауважте також, що при використанні урожайності, те, що ви повертаєте, не є типом типу функції. Це тип елемента в межахIEnumerable списку.

Ви використовуєте дохід із методом повернення методу як IEnumerable. Якщо тип повернення методу є intабо List<int>ви використовуєте yield, він не буде компілюватися. Ви можете використовувати IEnumerableтип повернення методу без урожайності, але, здається, можливо, ви не можете використовувати вихід без IEnumerableтипу повернення методу.

А щоб його виконати, потрібно викликати його особливим чином.

static void Main(string[] args)
{
    testA();
    Console.Write("try again. the above won't execute any of the function!\n");

    foreach (var x in testA()) { }


    Console.ReadLine();
}



// static List<int> testA()
static IEnumerable<int> testA()
{
    Console.WriteLine("asdfa");
    yield return 1;
    Console.WriteLine("asdf");
}

Примітка. Якщо ви намагаєтеся зрозуміти SelectMany, він використовує урожай, а також загальну інформацію. Цей приклад може допомогти public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { yield return t; }і public static IEnumerable<TResult> testYieldc<TResult>(TResult t) { return new List<TResult>(); }
barlop

Виглядає дуже добре пояснення! Це могла бути прийнятою відповіддю.
понгапундіт

@pongapundit дякую, моя відповідь, безумовно, чітка і проста, але я не використовував багато прибутків, інші відповідачі мають набагато більше досвіду роботи з ним та знання його використання, ніж я. Те, що я написав тут, приносив результат, мабуть, від того, щоб почухати голову, намагаючись з'ясувати деякі відповіді тут і за цим посиланням на dotnetperls! Але оскільки я не знаю yield returnцього добре (крім простої речі, про яку я згадував), і не використовував її багато і не знаю багато про її використання, я не думаю, що це має бути прийнятим.
барлоп

3

Одним з головних моментів щодо ключового слова Yield є Lazy Execution . Тепер, що я маю на увазі під ледачим виконанням, - це виконувати, коли потрібно. Кращий спосіб викласти це - наведення прикладу

Приклад: Не використовується вихід, тобто відсутність ледачого виконання.

        public static IEnumerable<int> CreateCollectionWithList()
        {
            var list =  new List<int>();
            list.Add(10);
            list.Add(0);
            list.Add(1);
            list.Add(2);
            list.Add(20);

            return list;
        }

Приклад: використання виходу, тобто ледачого виконання.

    public static IEnumerable<int> CreateCollectionWithYield()
    {
        yield return 10;
        for (int i = 0; i < 3; i++) 
        {
            yield return i;
        }

        yield return 20;
    }

Тепер, коли я називаю обидва методи.

var listItems = CreateCollectionWithList();
var yieldedItems = CreateCollectionWithYield();

ви помітите listItems матиме 5 елементів всередині нього (наведіть курсор миші на listItems під час налагодження). В той час як доходиItems будуть мати посилання лише на метод, а не на елементи. Це означає, що він не виконав процес отримання елементів всередині методу. Дуже ефективний спосіб отримання даних лише за потреби. Фактична реалізація врожайності спостерігається в таких органах, як Entity Framework та NHibernate тощо.


-3

Він намагається ввести трохи Ruby Goodness :)
Концепція: Це якийсь зразок Ruby Code, який друкує кожен елемент масиву

 rubyArray = [1,2,3,4,5,6,7,8,9,10]
    rubyArray.each{|x| 
        puts x   # do whatever with x
    }

Кожна реалізація методу масиву дає контроль над абонентом ("ставить х") з кожним елемент масиву акуратно представлений як x. Тоді, хто телефонує, може робити все, що потрібно для x.

Однак .Net не йде цілком сюди. Здається, що C # поєднав урожай з IEnumerable, таким чином, що змушує вас написати цикл foreach у абонента, як це видно у відповіді Менделта. Трохи менш елегантний.

//calling code
foreach(int i in obCustomClass.Each())
{
    Console.WriteLine(i.ToString());
}

// CustomClass implementation
private int[] data = {1,2,3,4,5,6,7,8,9,10};
public IEnumerable<int> Each()
{
   for(int iLooper=0; iLooper<data.Length; ++iLooper)
        yield return data[iLooper]; 
}

7
-1 Ця відповідь мені не звучить правильно. Так, C # yieldпоєднується з IEnumerable, а C # не має концепції Ruby "блоку". Але C # має лямбда, які могли б дозволити реалізувати ForEachметод, подібний Ruby each. Це не означає, що було б добре це зробити .
rsenna

Ще краще: public IEnumerable <int> Every () {int index = 0; дані про віддачу доходу [індекс ++]; }
ата
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.