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

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

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

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

16 ответов


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

когда вы пройдете через пример, вы найдете первый вызов целых чисел () возвращает 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);
                }
            }
        }
    }
}

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


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

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

  2. Это помогает выполнять итерацию с состоянием. enter image description here

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


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

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


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


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


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

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

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

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

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

Список Пример

    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
Привет, Боб!--4-->

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


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

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


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

блок итератора можно описать как синтаксический сахар, где компилятор генерирует машину состояния, которая отслеживает насколько перечисление перечисли прогрессировала. Чтобы перечислить перечисляемый, вы часто используете foreach петли. Однако,foreach loop также синтаксический сахар. Таким образом, вы-две абстракции, удаленные из реального кода, поэтому изначально может быть трудно понять, как все это работает вместе.

предположим, что у вас есть очень простой итератор блок:

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 операторы, чередующиеся с другим кодом.

для перечисления блока итератора a 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();
}

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

C# iterator block sequence diagram

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

каждый раз, когда вы вызываете блок итератора, создается новый экземпляр государственной машины. Однако, ваш код в итераторе блок выполняется до enumerator.MoveNext() выполняет впервые. Вот как работает отложенное выполнение. Вот (довольно глупый) пример:

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

на данный момент итератор не исполнены. The Where предложение создает новый IEnumerable<T>, которая включает IEnumerable<T> возвращено IteratorBlock но это перечисляемое еще не перечислено. Это происходит, когда вы выполняете foreach петли:

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

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

обратите внимание, что методы LINQ, такие как ToList(), ToArray(), First(), Count() etc. будет использовать foreach петли для перечисления перечисления. Например,ToList() перечислит все элементы перечисляемого и сохранит их в списке. Теперь вы можете получить доступ к списку, чтобы получить все элементы перечисляемого без повторного выполнения блока итератора. Существует компромисс между использованием CPU для создайте элементы перечисляемого несколько раз и память для хранения элементов перечисления для доступа к ним несколько раз при использовании таких методов, как ToList().


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

в 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 ints 4,4,4,4

здесь WriteLine. Добавит 4 в список, распечатает abc, затем добавит 4 в список, затем завершит метод и таким образом действительно вернется из метода (как только метод завершено, как это произошло бы с процедурой без возврата). Но это имело бы значение, an 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 тип возврата метода без выхода, но, похоже, вы не можете использовать выход без 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");
}

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

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

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

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

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

.Сети Не ходи сюда.. С# кажется, что в сочетании yield с 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]; 
}