2012-01-26 86 views
6

目前我正在閱讀流中的項目集合。我這樣做如下:收益總是被調用

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就是其中之一。

現在我注意到,我的空檢查是無用的?似乎每次我稱之爲功能時,它都會變成無用的。

有人可以向我解釋爲什麼這是?對於這種情況,最好的解決方案是什麼?(請告訴我,我所做的是完全廢話:P)。

+0

是的,它應該產生每次因爲你沒有任何地方分配「_items」變量所以它總是空,每次調用加載項目。 –

+0

請糾正我,如果我錯了,但不是我在這裏分配'_items = LoadItems()' –

回答

5

因爲LoadItems是一個懶惰的枚舉(使用yield)且將其分配給一個領域,這意味着你每次枚舉_items時候,你實際上是造成內LoadItems()環路再次運行,即(Enumerable.Count創建一個新的Enumerator每次導致LoadItems正文再次運行)。由於您不是每次都在LoadItems內重新創建閱讀器,因此其光標將位於流的末尾,因此可能無法再讀取更多行 - 我懷疑它正在返回null,並且您的單個Item對象返回第二個調用包含一個null字符串。

解決方案,這將是去「實現」的LoadItems結果通過調用Enumerable.ToList這將給你一個具體的列表:

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

或者尋求讀者回流的開始(如果可能)這樣LoadItems可以每次重複運行一次。

但我會建議你在這種情況下簡單地擺脫yield ing並返回一個具體的列表,因爲沒有什麼好處,所以你付出的複雜性價格沒有增益。

+0

這就是我想的。但是,當你詳盡地閱讀資源和收益率 - 返回項目。你如何確保你不會失去資源。 –

+0

您可以通過在生成的IEnumerable上調用'Enumerable.ToList'或通過手動構造'List <>'(或其他數據結構)來將讀取行實現爲具體列表,從而保留讀者的結果,例如LoadItems()。ToList()或新列表(LoadItems())'。 –

+0

啊,謝謝!我完全明白了產量問題!感謝您的解釋:) –

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

啊真棒,我真的很喜歡這個答案,因爲你解釋了什麼語法糖產量實際上! –

+0

不客氣。值得一提的是,如果在完成時將'use'塊放入'LoadItems'來處理'TextReader',那麼枚舉器的Dispose()'方法將會處理它(但不是枚舉,所以如果枚舉實際上沒有被枚舉,那麼它就不會被調用)。 –