我有一个linq查询,我在foreach循环中迭代结果。
第一个是从表格布局面板中抓取控件集合的查询,然后迭代集合并从tableLayoutPanel中删除控件:
var AllItems = (from Item in this.tableLayoutPanel1.Controls.OfType<ItemControl>()
select Item);
foreach (ItemControl item in AllItems)
{
Trace.WriteLine("Removing " + item.ToString());
this.tableLayoutPanel1.Controls.Remove(item);
item.Dispose();
}
以上没有按照我的预期进行(即抛出错误),它只删除了一半的控件(ODD编号为1) 似乎在每次迭代时AllItems都会减少它的自身,虽然底层集合正在被修改,但不会抛出任何错误。
如果我用一串字符串做同样的事情:
string[] strs = { "d", "c", "A", "b" };
List<string> stringList = strs.ToList();
var allitems = from letter in stringList
select letter;
foreach (string let in allitems)
{
stringList.Remove(let);
}
这次Visual Studio抛出一个错误(正如预期的那样)抱怨底层集合已经改变。
为什么第一个例子也不会爆炸?
有一些关于Iterators / IEnumerable的内容,我在这里并不理解,我想知道是否有人可以帮助我了解linq和foreach的内幕。
(我知道我可以通过AllItems.ToList()解决这两个问题;在迭代之前,但是想了解为什么第二个例子抛出错误而第一个没有错误)
答案 0 :(得分:10)
为什么第一个例子也不会爆炸?
IEnumerator
中的tableLayoutPanel1.Controls
实施未执行正确检查以查看集合是否已被修改。这并不意味着它是一个安全的操作(因为结果不合适),而是该类没有以引发异常的方式实现(可能应该这样)。
通过List<T>
枚举并删除项目时引发的异常实际上是由List<T>.Enumerator
类型引发的,并且不是“通用”语言功能。
请注意,这是List<T>.Enumerator
类型的精彩功能,因为IEnumerator<T>
文档明确指出:
只要集合保持不变,枚举器仍然有效。如果对集合进行了更改,例如添加,修改或删除元素,则枚举数将无法恢复,并且其行为未定义。
虽然没有合同要求抛出异常,但当您进入“未定义行为”的领域时,实现这样做是有益的。
在这种情况下,TableLayoutControlCollection类枚举器实现不执行额外检查,因此您将看到当您使用“行为未定义”的功能时会发生什么。 (在这种情况下,它会删除所有其他控件。)
答案 1 :(得分:2)
所以在我的第一个案例中,linq表达式被重新评估 foreach循环的迭代?或者是linq从中获取IEnumerator tablelayoutpanel.controls集合?
把它想象成就像这样发生在你的两种情况下发生的事情:
foreach (string l in (from letter in stringList select letter))
{
stringList.Remove(l);
}
上面的伪代码说明当您尝试删除项目时,您基本上处于stringList
集合的枚举中。里德所说的是,在第二种情况下,stringList
的调查员确保在枚举的过程中没有人弄乱它。
他说,对于你的控件,控件枚举器很乐意继续进行,而不是检查是否有人在调整其数据。
为了完整起见,请比较以下两个例子:
每次触摸letters
时,都会重新评估letters
的此查询,其中包括我们在foreach循环中的时间:
IEnumerable<string> letters = from letter in stringList select letter);
// everytime we hit this we're going to hit the stringList collection
foreach (string l in letters)
{
stringList.Remove(l);
}
此查询使用ToList()立即填充letters
,我们将不再触及foreach循环中的stringList
集合。
List<string> letters = (from letter in stringList select letter).ToList()
// because we ran ToList(), we will no longer enumerate stringList
foreach (string l in letters)
{
stringList.Remove(l);
}
答案 2 :(得分:0)
试试这个:
var AllItems = (from Item in this.tableLayoutPanel1.Controls.OfType<ItemControl>()
select Item);
int totalCount = AllItems .Count;
for (int i = 0; i < totalCount; i++)
{
AllItems .RemoveAt(0);
}