屈服总是被召唤

时间:2012-01-26 10:50:17

标签: c# ienumerable yield

目前我正在阅读流中的一组项目。我这样做如下:

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 个答案:

答案 0 :(得分:5)

因为LoadItems是一个懒惰的可枚举(使用yield)并且您将其分配给一个字段,这意味着每次枚举_items时,您实际上都会导致LoadItems()内的循环{1}}再次运行,即(Enumerable.Count每次都会创建一个新Enumerator,导致LoadItems正文再次运行。由于您不是每次在LoadItems内重新创建阅读器,其光标将位于流的末尾,因此可能无法再读取任何行 - 我怀疑它正在返回null并且您在第二次调用时返回的单个Item对象包含null字符串。

解决这个问题的方法是通过调用LoadItems来“实现”Enumerable.ToList的结果,这会给你一个具体的列表:

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

或者让读者回到流的开头(如果可能的话),这样LoadItems每次都可以再次运行。

但是我建议你在这种情况下简单地摆脱yield并返回一个具体的清单,因为没有什么好处所以你付出了复杂的价格而没有收获。

答案 1 :(得分:1)

你的变量名让你误入歧途。目前:

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

答案 2 :(得分:1)

考虑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();