[英]Why can some Enumerable be changed inside foreach, and others can't?
在使用 C# 時,我發現 LINQ 查詢結果的一個有趣行為。 我試圖弄清楚這一點,但找不到正確解釋為什么會這樣工作。 所以我在這里問,也許有人可以給我一個很好的解釋(導致這種行為的內部工作)或者一些鏈接。
我有這個 class:
public class A
{
public int Id { get; set; }
public int? ParentId { get; set; }
}
而這個 object:
var list = new List<A>
{
new A { Id = 1, ParentId = null },
new A { Id = 2, ParentId = 1 },
new A { Id = 3, ParentId = 1 },
new A { Id = 4, ParentId = 3 },
new A { Id = 5, ParentId = 7 }
};
還有我的代碼,它適用於這個 object:
var result = list.Where(x => x.Id == 1).ToList();
var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId));
Console.WriteLine(result.Count); // 1
Console.WriteLine(valuesToInsert.Count()); //2
foreach (var value in valuesToInsert)
{
result.Add(value);
}
Console.WriteLine(valuesToInsert.Count()); //3. collection (and its count) was changed inside the foreach loop
Console.WriteLine(result.Count); //4
因此, result
變量的計數為 1, valuesToInsert
計數為 2,並且在 foreach 循環(不會顯式更改valuesToInsert
)之后, valuesToInsert
的計數正在更改。 而且,盡管在開始時valuesToInsert
的foreach
計數是2 ,但foreach
進行了三次迭代。
那么為什么這個 Enumerable 的值可以在foreach
中改變呢? 並且,例如,如果我使用此代碼更改 Enumerable 的值:
var testEn = list.Where(x => x.Id == 1);
foreach (var x in testEn)
{
list.Add(new A { Id = 1 });
}
我得到System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'
System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'
. 它們之間有什么區別? 為什么一個集合可以修改而另一個不能?
PS如果我像這樣添加ToList()
:
var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId)).ToList();
或者像這樣:
foreach (var value in valuesToInsert.ToList())
它只進行兩次迭代。
這里有多個問題:
因此,在第一次查詢結果變量的計數為 1 之后,在第二次查詢 valuesToInsert 計數為 2 之后,並且在 foreach 循環(不會顯式更改 valuesToInsert)之后,valuesToInsert 的計數正在發生變化。
正如預期的那樣,因為我們在變量中的引用與valuesToInsert
變量所持有的引用相同。 所以 object 是相同的,但多個引用指向同一個。
你的第二個問題:
那么為什么這個 Enumerable 的值可以在 foreach 中改變呢?
當我們將集合作為 IEnumerable 類型的引用時,IEnumerable 集合是只讀的,但是當我們在其上調用ToList()
方法時,我們有一個指向同一個原始集合的集合的副本,但是我們現在可以向集合中添加更多項目.
當我們將集合設置為IEnumerable
時,可以迭代和讀取集合,但是在枚舉時添加更多項目會失敗,因為應該按順序讀取集合。
第三:
它只進行兩次迭代。
是的,因為在那個時候,無論集合中的項目數量是多少,並且對它的引用都被存儲為一個新列表,而它仍然指向相同的 object 即 IEnumerable 但現在我們可以添加更多項目,因為它的類型作為列表。
看:
var result = list.Where(x => x.Id == 1).ToList();
// result is collection which can be modified, items add, remove etc
var result = list.Where(x => x.Id == 1);
// result is IEnumerable which can be iterated to get items one by one
// modifying this collection would error out normally
valuesToInsert集合在Where
子句中引用了結果集合:
var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId));
因為 Enumerable 使用 yield return 工作,所以它使用最近生成的每個項目的結果集合。
如果您不希望這種行為,您應該首先使用ToList()
評估valueToInsert
foreach (var value in valuesToInsert.ToList())
關於“收藏已修改”異常。 枚舉時不能更改枚舉。 現在結果集合已更改,但在枚舉時不會更改; 僅在每次 for each 循環請求新項目時才會枚舉它。 (這使您添加子代的算法效率降低,這對於巨大的 collections 將變得明顯。)
這段代碼:
foreach (var value in valuesToInsert)
{
result.Add(value);
}
...由 C# 編譯器轉換為等效的代碼塊:
IEnumerator<A> enumerator = valuesToInsert.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
var value = enumerator.Current;
result.Add(value);
}
}
finally
{
enumerator.Dispose();
}
當List
發生變異時, List
返回的枚舉數無效,這意味着如果在變異后調用MoveNext
方法,它將拋出InvalidOperationException
。 在這種情況下, valuesToInsert
不是List
,而是由 LINQ 方法返回的枚舉Where
。 該方法通過枚舉其源(在本例中為list
)懶惰地獲得的枚舉器來工作。 所以枚舉一個枚舉器間接導致另一個枚舉器的枚舉,它隱藏在神奇的 LINQ 鏈中更深。 在第一種情況下, list
不會在枚舉塊內發生變化,因此不會引發異常。 在第二種情況下,它發生了變異,導致異常從一個MoveNext
傳播到另一個,並最終由foreach
語句拋出。
值得注意的是,此行為不屬於List
class 的公共合同的一部分,因此可以在 .NET 的未來版本中進行更改。 因此,您可能應該避免依賴這種行為來確保程序的正確性。 這個警告不是理論上的。 .NET Core 3.0 中的Dictionary
class 已經發生了這樣的變化。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.