Какое ключевое слово yield используется для С#?

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

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

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

573
02 сент. '08 в 16:15
источник поделиться
16 ответов

Ключевое слово yield действительно здесь довольно много. Функция возвращает объект, реализующий интерфейс IEnumerable. Если вызывающая функция запускается для этого объекта, функция вызывается снова, пока она не "выйдет". Это синтаксический сахар, введенный в С# 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);
                }
            }
        }
    }
}
520
02 сент. '08 в 16:23
источник

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

301
02 сент. '08 в 16:17
источник

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

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

117
02 сент. '08 в 16:27
источник

Выход имеет два больших использования,

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

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

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

113
12 апр. '13 в 20:31
источник

На первый взгляд, доходность возврата - это сахар .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();
    }
}

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

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

27
17 янв. '15 в 17:23
источник

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

24
25 февр. '14 в 20:15
источник

Интуитивно ключевое слово возвращает значение из функции, не оставляя его, то есть в вашем примере кода оно возвращает текущее значение item, а затем возобновляет цикл. Более формально он используется компилятором для генерации кода для итератора. Итераторы - это функции, возвращающие объекты IEnumerable. MSDN содержит несколько статей о них.

24
02 сент. '08 в 16:19
источник

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

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

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

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

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

Привет, Bob

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

12
04 дек. '15 в 22:52
источник

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

Подумайте об этом так: Вы идете на счетчик мяса и хотите купить фунт нарезанной ветчины. Мясник берет 10-фунтовую ветчину на спину, кладет ее на машину для резки, нарезает все это, затем возвращает кучу ломтиков вам и измеряет фунт. (OLD). С помощью yield мясник выводит машину сглаживателя на счетчик и начинает нарезать и "уронить" каждый срез на шкале до тех пор, пока он не будет измерять 1 фунт, а затем обертывает его для вас, и все готово. Старый путь может быть лучше для мясника (позволяет ему организовывать свою технику так, как ему нравится), но в большинстве случаев новый способ явно более эффективен для потребителя.

9
01 сент. '16 в 16:43
источник

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

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

6
15 янв. '13 в 1:49
источник

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

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

Вот результат (здесь нет сюрпризов):

Begin
1
After 1
2
After 2
42
End

Как указано выше foreach является синтаксическим сахаром:

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

В попытке распутать это я разбил диаграмму последовательности с удаленными абстракциями:

Диаграмма последовательности блоков итератора С#

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

Каждый раз, когда вы вызываете свой блок итератора, создается новый экземпляр конечного автомата. Однако ни один из ваших кодов в блоке итератора не выполняется до тех пор, пока 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().

4
28 июня '17 в 14:54
источник

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

4
02 сент. '08 в 16:17
источник

Как только вы хорошо понимаете, как работают итераторные блоки, У Эрика Липперта отличная серия сообщений в блоге на некоторых, казалось бы, нечетных ограничения на общность блоков итератора.

3
04 дек. '17 в 3:46

Он производит перечислимую последовательность. То, что он делает, фактически создает локальную последовательность IEnumerable и возвращает ее как результат метода

2
02 сент. '08 в 16:18
источник

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

Здесь даже более простые примеры

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 список int s, который он возвращает по завершении.

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

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

Вы используете yield с возвращаемым типом метода как IEnumerable. Если тип возвращаемого метода - int или List<int>, и вы используете yield, то он не будет компилироваться. Вы можете использовать тип возвращаемого метода IEnumerable без урока, но, похоже, вы не можете использовать выход без IEnumerable метода return type.

И чтобы заставить его выполнить, вы должны вызвать его особым образом.

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");
}
0
01 мая '16 в 4:25
источник

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

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

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

Однако .Net не идет полным ходом здесь. С#, кажется, связал выход с 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]; 
}
-1
02 сент. '08 в 17:06
источник

Посмотрите другие вопросы по меткам или Задайте вопрос