Для чего используется ключевое слово yield в C #? - PullRequest
740 голосов
/ 02 сентября 2008

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

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

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

Ответы [ 16 ]

654 голосов
/ 02 сентября 2008

Ключевое слово 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);
                }
            }
        }
    }
}
347 голосов
/ 02 сентября 2008

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

184 голосов
/ 12 апреля 2013

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

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

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

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

128 голосов
/ 02 сентября 2008

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

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

71 голосов
/ 17 января 2015

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

34 голосов
/ 25 февраля 2014

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

30 голосов
/ 02 сентября 2008

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

24 голосов
/ 04 декабря 2015

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

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

22 голосов
/ 01 сентября 2016

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

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

13 голосов
/ 28 июня 2017

Ключевое слово 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);

Вот вывод (здесь никаких сюрпризов):

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);

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...