Для чего используется ключевое слово yield в C #?


830

В вопросе Как я могу раскрыть только фрагмент IList <> у одного из ответов был следующий фрагмент кода:

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

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


Просто ссылка MSDN об этом здесь msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx
Разработчик

14
Это не удивительно. Путаница возникает из-за того, что мы привыкли видеть «возврат» как выход функции, а перед ним «выход», а это не так.
Ларри

4
Я прочитал документы, но боюсь, что до сих пор не понимаю :(
Ortund

Ответы:


739

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()return 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);
                }
            }
        }
    }
}

114
В этом случае это будет проще, я просто использую здесь целое число, чтобы показать, как работает возврат прибыли. Преимущество использования yield return заключается в том, что это очень быстрый способ реализации шаблона итератора, поэтому все оценивается лениво.
Мендель

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

7
yieldэто не ключевое слово Если бы это было так, я бы не смог использовать yield в качестве идентификатора, как вint yield = 500;
Brandin

5
@Brandin потому, что все языки программирования поддерживают два типа ключевых слов, а именно зарезервированные и контекстные. yield относится к более поздней категории, поэтому ваш код не запрещен компилятором C #. Подробнее здесь: ericlippert.com/2009/05/11/reserved-and-contextual-keywords Вам будет приятно узнать, что существуют также зарезервированные слова, которые не распознаются в качестве ключевых слов языком. Например, goto в 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 # yield в контексте «урожай дает обильный урожай», а не «автомобиль уступает пешеходу».
Зак

370

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


211

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

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

  2. Это помогает делать итерацию с учетом состояния. введите описание изображения здесь

Чтобы более наглядно объяснить вышеупомянутые два момента, я создал простое видео, которое вы можете посмотреть здесь.


14
Видео поможет мне четко понять yield. Статья проекта кода @ ShivprasadKoirala Какая польза от C # Yield? того же объяснения также является хорошим источником
Dush

Я бы также добавил в качестве третьего пункта, что yieldэто «быстрый» способ создания пользовательского IEnumerator (вместо того, чтобы класс реализовывал интерфейс IEnumerator).
MrTourkos

Я посмотрел ваше видео Shivprasad, и оно четко объяснило использование ключевого слова yield.
Торе Аурстад

Спасибо за видео! Очень хорошо объяснил!
Roblogic

Отличное видео, но интересно ... Реализация, использующая yield, явно чище, но, по сути, она должна создавать собственную временную память или / и внутренний список для отслеживания состояния (или, скорее, создания конечного автомата). Итак, «Выход» делает что-то еще, кроме упрощения реализации и улучшения внешнего вида, или есть что-то еще? Как насчет эффективности, работает ли код с использованием Yield более или менее эффективно / быстро, чем без него?
ToughQuestions

135

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

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


За это нужно проголосовать. Сладко, как он объясняет цель оператора и внутренних органов.
Саджиднизами

3
Часть 1 объясняет синтаксический сахар «доходность». отлично объясняю!
Дрор Вайс

99

На первый взгляд возвращаемая доходность - это сахар .NET, возвращающий IEnumerable .

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

class SomeData
{
    public SomeData() { }

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

Тот же код с помощью yield, он возвращает элемент за элементом:

class SomeData
{
    public SomeData() { }

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

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

Оператор yield позволяет создавать элементы по мере необходимости. Это хорошая причина, чтобы использовать его.


40

yield returnиспользуется с счетчиками. При каждом вызове оператора yield управление возвращается вызывающей стороне, но оно обеспечивает поддержание состояния вызываемой стороны. Вследствие этого, когда вызывающий объект перечисляет следующий элемент, он продолжает выполнение в методе вызываемого оператора сразу после 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

Реализация списка или массива загружает все элементы немедленно, тогда как реализация yield обеспечивает решение отложенного выполнения.

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

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

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

Вот сравнение между созданием первой коллекции, такой как список, по сравнению с использованием yield.

Пример списка

    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
Hello Bob

Ницца! Только первый контакт был создан, когда клиент «вытащил» элемент из коллекции.


1
Этот ответ требует большего внимания! Thx
leon22

@ leon22 абсолютно +2
snr

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 с yield.

  • Вот один
  • Звоните еще раз, если вам нужен другой.
  • Я буду помнить то, что я тебе уже дал.
  • Я буду знать только, смогу ли я дать вам еще один, когда вы снова позвоните.

просто и блестяще
Гарри

10

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

В JavaScript та же концепция называется Генераторы.


Лучшее объяснение еще. Это тоже генераторы в питоне?
Petrosmm

7

Это очень простой и легкий способ создать перечисляемый для вашего объекта. Компилятор создает класс, который оборачивает ваш метод и реализует, в данном случае, IEnumerable <объект>. Без ключевого слова yield вам нужно создать объект, реализующий IEnumerable <объект>.


5

Это производит перечисляемую последовательность. На самом деле он создает локальную последовательность IEnumerable и возвращает ее как результат метода


3

Эта ссылка имеет простой пример

Еще более простые примеры здесь

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

Обратите внимание, что возвращаемая доходность не будет возвращаться из метода. Вы можете даже положить WriteLineпослеyield return

Выше приведено IEnumerable 4 целых 4,4,4,4

Здесь с WriteLine. Добавьте 4 в список, напечатайте abc, затем добавьте 4 в список, затем завершите метод и, таким образом, действительно вернитесь из метода (как только метод завершится, как это будет происходить с процедурой без возврата). Но это будет иметь значение, IEnumerableсписок ints, который он возвращает по завершении.

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

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

Вы используете yield с типом возврата метода как IEnumerable. Если тип возвращаемого метода равен intили, List<int>и вы используете yield, то он не скомпилируется. Вы можете использовать IEnumerableметод возврата типа без yield, но, возможно, вы не можете использовать yield без 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, он использует yield, а также дженерики ... этот пример может помочь 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 . Теперь, что я подразумеваю под 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;
        }

Пример: использование Yield т.е. Lazy Execution.

    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 во время отладки). Тогда как yieldItems будет просто иметь ссылку на метод, а не на элементы. Это означает, что он не выполнил процесс получения элементов внутри метода. Очень эффективный способ получения данных только при необходимости. Реальную реализацию yield можно увидеть в ORM, таких как Entity Framework, NHibernate и т. Д.


-3

Он пытается ввести некоторую
концепцию Ruby Goodness :) : это пример кода Ruby, который распечатывает каждый элемент массива

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

Массива каждая реализация методы дает контроль над вызывающим абонентом (в «пут х») с каждым элементом массива аккуратно представлены в виде х. Затем вызывающая сторона может делать с x все, что ей нужно.

Однако .Net здесь не доходит до конца. Кажется, что C # связал выход с IEnumerable, что заставило вас написать цикл вызова в вызывающей программе, как видно из ответа Менделя. Немного менее элегантно.

//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> Each () {int index = 0; yield return data [index ++]; }
ата
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.