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

StackOverflow https://stackoverflow.com/questions/39476

  •  09-06-2019
  •  | 
  •  

Вопрос

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

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

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

Это было полезно?

Решение

Ключевое слово yield здесь на самом деле очень много.

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

Другие советы

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

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

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

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

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

    Недавно Рэймонд Чен также опубликовал интересную серию статей по ключевому слову 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 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(), и состояние для него будет сброшено.

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

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

    Консольный вывод
    Магазин контактных данных:Создание контакта 1
    Магазин контактных данных:Создание контакта 2
    Магазин контактных данных:Создание контакта 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();
    }
    

    Консольный вывод
    Готовы к повторному просмотру коллекции
    Контактный полевой магазин:Создание контакта 1
    Привет, Боб

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

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

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

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

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

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

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

    Реальные блоки итераторов часто имеют условия и циклы, но когда вы проверяете условия и разворачиваете циклы, они все равно заканчиваются выражениями Where, чередующимися с другим кодом.

    Для перечисления блока итератора используется цикл IteratorBlock:

    foreach (var i in IteratorBlock())
        Console.WriteLine(i);
    

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

    Begin
    1
    After 1
    2
    After 2
    42
    End
    

    Как указано выше ToList() это синтаксический сахар:

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

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

     Блок-схема итератора C #

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

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

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

    На данный момент итератор не выполнен. Предложение First() создает новое Count(), которое оборачивает <=>, возвращаемое <=>, но это перечислимое еще предстоит перечислить. Это происходит, когда вы выполняете цикл <=>:

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

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

    Обратите внимание, что методы LINQ, такие как <=>, <=>, <=>, <=> и т. д., будут использовать цикл <=> для перечисления перечислимого. Например, <=> перечислит все элементы перечислимого и сохранит их в списке. Теперь вы можете получить доступ к списку, чтобы получить все элементы перечислимого без повторного выполнения блока итератора. Существует компромисс между использованием ЦП для создания элементов перечислимого несколько раз и памяти для хранения элементов перечисления для многократного доступа к ним при использовании таких методов, как <=>.

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

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

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

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

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

    Это производит перечислимую последовательность. На самом деле он создает локальную последовательность 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

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

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

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

    Вы используете yield с типом возвращаемого значения метода как <=>. Если тип возврата метода <=> или <=> и вы используете <=>, он не будет компилироваться. Вы можете использовать <=> метод возврата типа без yield, но, возможно, вы не можете использовать yield без <=> метода возврата метода.

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

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

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

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

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

    Однако .Net здесь не доходит до конца .. Кажется, что C # совпал с 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]; 
    }
    
    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top