为什么有些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; }
}
这个对象:
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 }
};
我的代码,适用于这个对象:
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
开始时计数是 两个 ,但 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.'
。它们之间有什么区别?为什么一个合集可以修改,另一个不可以?
P.S。如果我像这样添加 ToList()
:
var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId)).ToList();
或者像这样:
foreach (var value in valuesToInsert.ToList())
它只进行两次迭代。
这里有多个问题:
So, after first query Count of result variable is 1, after second query valuesToInsert count is 2, and after the foreach loop (which doesn't change the valuesToInsert explicitly) count of the valuesToInsert is changing.
这是预期的,因为我们在变量中的引用与 valuesToInsert
变量的引用相同。所以 object 是相同的,但多个引用指向同一个。
你的第二个问题:
So why value of this Enumerable can be changed inside foreach?
当我们将 collection 作为 IEnumerable 类型的引用时,IEnumerable collection 是只读的,但是当我们对其调用 ToList()
方法时,我们拥有 [=33= 的副本] 指向相同的原始 collection 但我们现在可以向 collection 添加更多项目。
当我们将 collection 作为 IEnumerable
时,可以迭代和读取 collection,但是在枚举时添加更多项目将失败,因为应该读取 collection按顺序。
第三个:
It makes only two iterations.
是的,因为在那一刻枚举了集合中的项目数量,并且对它的引用被存储为一个新列表,同时它仍然指向相同的 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
子句中引用了 result 集合:
var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId));
因为 Enumerable 使用 yield return 工作,它使用最新的 result 集合来生产每个项目。
如果您不希望出现这种情况,您应该首先使用 ToList()
评估 valueToInsert
foreach (var value in valuesToInsert.ToList())
关于 'Collection was modified' 异常。您不能在枚举时更改枚举。现在 result 集合已更改,但在枚举时不会更改;每次 for each 循环请求一个新项目时,它只会被枚举。 (这会使您添加子项的算法效率降低,这对于大型集合会变得很明显。)
这段代码:
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 的 public 契约的一部分,因此它可能会在未来的 .NET 版本中更改。因此,您应该避免依赖此行为来确保程序的正确性。这个警告不是理论上的。 .NET Core 3.0 中 与 Dictionary
class 的类似更改。
我在使用 C# 时发现了 LINQ 查询结果的一个有趣行为。我试图弄清楚这一点,但找不到正确的解释为什么它会按原样工作。所以我在这里问,也许有人可以给我一个很好的解释(导致这种行为的内部工作)或者一些链接。
我有这个class:
public class A
{
public int Id { get; set; }
public int? ParentId { get; set; }
}
这个对象:
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 }
};
我的代码,适用于这个对象:
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
开始时计数是 两个 ,但 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.'
。它们之间有什么区别?为什么一个合集可以修改,另一个不可以?
P.S。如果我像这样添加 ToList()
:
var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId)).ToList();
或者像这样:
foreach (var value in valuesToInsert.ToList())
它只进行两次迭代。
这里有多个问题:
So, after first query Count of result variable is 1, after second query valuesToInsert count is 2, and after the foreach loop (which doesn't change the valuesToInsert explicitly) count of the valuesToInsert is changing.
这是预期的,因为我们在变量中的引用与 valuesToInsert
变量的引用相同。所以 object 是相同的,但多个引用指向同一个。
你的第二个问题:
So why value of this Enumerable can be changed inside foreach?
当我们将 collection 作为 IEnumerable 类型的引用时,IEnumerable collection 是只读的,但是当我们对其调用 ToList()
方法时,我们拥有 [=33= 的副本] 指向相同的原始 collection 但我们现在可以向 collection 添加更多项目。
当我们将 collection 作为 IEnumerable
时,可以迭代和读取 collection,但是在枚举时添加更多项目将失败,因为应该读取 collection按顺序。
第三个:
It makes only two iterations.
是的,因为在那一刻枚举了集合中的项目数量,并且对它的引用被存储为一个新列表,同时它仍然指向相同的 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
子句中引用了 result 集合:
var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId));
因为 Enumerable 使用 yield return 工作,它使用最新的 result 集合来生产每个项目。
如果您不希望出现这种情况,您应该首先使用 ToList()
foreach (var value in valuesToInsert.ToList())
关于 'Collection was modified' 异常。您不能在枚举时更改枚举。现在 result 集合已更改,但在枚举时不会更改;每次 for each 循环请求一个新项目时,它只会被枚举。 (这会使您添加子项的算法效率降低,这对于大型集合会变得很明显。)
这段代码:
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 的 public 契约的一部分,因此它可能会在未来的 .NET 版本中更改。因此,您应该避免依赖此行为来确保程序的正确性。这个警告不是理论上的。 .NET Core 3.0 中 Dictionary
class 的类似更改。