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

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

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

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

20 ответов

Решение

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

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

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

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

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

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

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

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

На первый взгляд возвращаемая доходность - это сахар.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 позволяет создавать элементы по мере необходимости. Это хорошая причина, чтобы использовать его.

Реализация списка или массива загружает все элементы немедленно, тогда как реализация 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
Привет боб

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

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() метод, то состояние будет сброшено для него.

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

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

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

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

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

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(),

Одним из основных моментов ключевого слова Yield является Lazy Execution. Под "ленивым выполнением" я подразумеваю выполнение при необходимости. Лучше выразиться, приведя пример

Пример: без использования Yield, т.е. без ленивого выполнения.

        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 и т. Д.

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

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

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

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

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

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

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с, что он возвращается по завершении.

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

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

Вы используете yield с типом возврата метода как 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");
}

В настоящее время вы можете использовать yield ключевое слово для асинхронных потоков.

В C# 8.0 представлены асинхронные потоки, которые моделируют потоковый источник данных. Потоки данных часто извлекают или генерируют элементы асинхронно. Асинхронные потоки полагаются на новые интерфейсы, представленные в .NET Standard 2.1. Эти интерфейсы поддерживаются в .NET Core 3.0 и более поздних версиях. Они обеспечивают естественную модель программирования для источников асинхронных потоковых данных.

Источник: Microsoft docs

Пример ниже

      using System;
using System.Collections.Generic;               
using System.Threading.Tasks;

public class Program
{
    public static async Task Main()
    {
        List<int> numbers = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        
        await foreach(int number in YieldReturnNumbers(numbers))
        {
            Console.WriteLine(number);
        }
    }
    
    public static async IAsyncEnumerable<int> YieldReturnNumbers(List<int> numbers) 
    {
        foreach (int number in numbers)
        {
            await Task.Delay(1000);
            yield return number;
        }
    }
}

Простая демонстрация для понимания yield

      using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp_demo_yield {
    class Program
    {
        static void Main(string[] args)
        {
            var letters = new List<string>() { "a1", "b1", "c2", "d2" };

            // Not yield
            var test1 = GetNotYield(letters);

            foreach (var t in test1)
            {
                Console.WriteLine(t);
            }

            // yield
            var test2 = GetWithYield(letters).ToList();

            foreach (var t in test2)
            {
                Console.WriteLine(t);
            }

            Console.ReadKey();
        }

        private static IList<string> GetNotYield(IList<string> list)
        {
            var temp = new List<string>();
            foreach(var x in list)
            {
                
                if (x.Contains("2")) { 
                temp.Add(x);
                }
            }

            return temp;
        }

        private static IEnumerable<string> GetWithYield(IList<string> list)
        {
            foreach (var x in list)
            {
                if (x.Contains("2"))
                {
                    yield return x;
                }
            }
        }
    } 
}

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

Он пытается внести немного Руби Боже:)
Концепция: это пример кода Ruby, который распечатывает каждый элемент массива

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

Каждая реализация метода Array дает контроль вызывающей стороне ("ставит x"), где каждый элемент массива аккуратно представлен как 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]; 
}
Другие вопросы по тегам