簡體   English   中英

為什么有些 Enumerable 可以在 foreach 中更改,而有些則不能?

[英]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的計數正在更改。 而且,盡管在開始時valuesToInsertforeach計數是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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM