yield всегда вызывается - PullRequest
6 голосов
/ 26 января 2012

В настоящее время я читаю коллекцию предметов из потока. Я делаю это следующим образом:

public class Parser{

 private TextReader _reader; //Get set in Constructor
 private IEnumerable<Item> _items;    

 public IEnumerable<Item> Items{
  get{
   //I >>thought<< this would prevent LoadItems() from being called twice.
   return _items ?? (_items = LoadItems());
  }
 }

 public IEnumerable<Item> LoadItems(){
  while(_reader.Peek() >= 0){
   yield return new Item(_reader.ReadLine()); //Actually it's a little different
  }
 }
}

Допустим, у меня есть поток, который содержит два элемента, и я делаю следующее:

var textReader = //Load textreader here
var parser = new Parser(textReader);
var result1 = parser.Items.Count();
var result2 = parser.Items.Count();

Теперь result1 равно 2, а result2 равно 1.

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

Может кто-нибудь объяснить мне, почему это так? И какое было бы наилучшее решение для этой ситуации (пожалуйста, скажите мне, если я делаю полную чушь: P).

Ответы [ 3 ]

5 голосов
/ 26 января 2012

Поскольку LoadItems является ленивым перечислимым (использует yield), и вы присваиваете его полю, это означает, что каждый раз, когда вы перечисляете _items, вы фактически заставляете цикл внутри LoadItems() запускаться снова , т. е. (Enumerable.Count каждый раз создает новый Enumerator, что приводит к повторному запуску тела LoadItems). Поскольку вы не создаете устройство чтения заново каждый раз в пределах LoadItems, его курсор будет расположен в конце потока, поэтому, скорее всего, не сможете читать больше строк - я подозреваю, что он возвращает null и ваш единственный Item объект, возвращаемый во втором вызове, содержит строку null.

Решением этой проблемы будет «осознание» результата LoadItems путем вызова Enumerable.ToList, который даст вам конкретный список:

return _items ?? (_items = LoadItems().ToList());

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

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

1 голос
/ 26 января 2012

Учтите, что yield используется как короткая рука. Ваш код превращается во что-то вроде:

private class <>ImpossibleNameSoItWontCollide : IEnumerator<Item>
{
   private TextReader _rdr;
   /* other state-holding fields */
   public <>ImpossibleNameSoItWontCollide(TextReader rdr)
   {
     _rdr = rdr;
   }
   /* Implement MoveNext, Current here */
}
private class <>ImpossibleNameSoItWontCollide2 : IEnumerable<Item>
{
   private TextReader _rdr;
   /* other state-holding fields */
   public <>ImpossibleNameSoItWontCollide2(TextReader rdr)
   {
     _rdr = rdr;
   }
   public <>ImpossibleNameSoItWontCollide GetEnumerator()
   {
     return new <>ImpossibleNameSoItWontCollide(_rdr);
   }
   /* etc */
}
public IEnumerable<Item> LoadItems()
{
    return new <>ImpossibleNameSoItWontCollide2(_rdr);
}

Следовательно, LoadItems() действительно вызывается только один раз, но объект, который он возвращает, имеет GetEnumerator(), вызываемый дважды.

И так как TextReader перешел, это дает неверные результаты для вас. Однако обратите внимание, что это приводит к меньшему использованию памяти, чем к удержанию всех элементов, поэтому у него есть преимущества, если вы не хотите использовать один и тот же набор элементов дважды.

Так как вы делаете хотите, вам нужно создать объект, который хранит их:

return _items = _items ?? _items = LoadItems().ToList();
1 голос
/ 26 января 2012

Ваше имя переменной сбило вас с пути. На данный момент:

 private IEnumerable<Item> _items; 

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

public class Parser{

 private TextReader _reader; //Get set in Constructor
 private List<Item> _items;    

 public IEnumerable<Item> Items{
  get{
   return _items ?? (_items = LoadItems().ToList());
  }
 }

 private IEnumerable<Item> LoadItems(){
  while(_reader.Peek() >= 0){
   yield return new Item(_reader.ReadLine()); //Actually it's a little different
  }
 }
}
...