2008-09-02 7 views
611

В How Can I Expose Only a Fragment of IList<> вопрос один из ответов был следующий фрагмент кода:Какое ключевое слово yield используется для C#?

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

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

+0

Просто ссылка MSDN об этом здесь http://msdn.microsoft.com/en-us/library/vstudio/9k7k7cf0.aspx – 2013-04-24 18:44:36

+9

Это неудивительно. Путаница исходит из того факта, что мы обусловлены тем, что «возвращаемся» как выход функции, а впереди «выход» - нет. – Larry 2014-02-20 11:37:01

+1

Вот очень хорошее техническое объяснение: http://blogs.msdn.com/oldnewthing/archive/2008/08/12/8849519.aspx – Nir 2008-09-02 13:21:15

ответ

544

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

В этом случае это было бы проще, я просто использую целое число здесь, чтобы показать, как работает доход возврата. Хорошие вещи об использовании доходности возврата - это очень быстрый способ реализации шаблона итератора, поэтому все оценивается ленивым. – Mendelt 2008-12-22 08:35:58

+86

Также стоит отметить вы можете использовать `break break`, когда вы не хотите возвращать больше элементов. – Rory 2011-05-17 18:13:03

309

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

4

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

2

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

24

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

+1

Ну, если быть точным, он не возобновляет цикл, он приостанавливает его, пока родитель не называет «iterator.next()». – Alex 2013-07-10 12:15:22

+5

@jitbit Вот почему я использовал «интуитивно» и «более формально». – 2013-07-10 12:16:59

118

Недавно Raymond Chen также провели интересную серию статей по ключевому слову доходности.

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

-2

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

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 не пройти весь путь здесь .. C#, кажется, в сочетании с выходом IEnumerable, таким образом, заставляя вас написать цикл Еогеасп в вызывающем, как показано в ответ Mendelt в. Немного менее изящный.

//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]; 
} 
6

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

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

124

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

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

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

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

26

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

34

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

14

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

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

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

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

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

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

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

Console Output
Готов перебирать коллекцию
ContactYieldStore: Создание контакта 1
Здравствуйте Боб

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

0

Это link имеет простой пример,

Даже простые примеры здесь

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

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

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

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

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

8

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

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

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

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

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() перечислит все элементы перечислимого и сохранит их в списке. Теперь вы можете получить доступ к списку, чтобы получить все элементы перечисляемого без повторного выполнения блока итератора. Существует компромисс между использованием CPU для создания элементов перечислимого множества раз и памяти для хранения элементов перечисления для доступа к ним несколько раз при использовании таких методов, как ToList().

Смежные вопросы