【问题标题】:foreach and linq query - need help trying to understand pleaseforeach 和 linq 查询 - 请帮助理解
【发布时间】:2012-07-18 22:45:43
【问题描述】:

我有一个 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();
}

上面没有按我的预期做(即抛出错误),它只删除了一半的控件(奇数编号的) 似乎在每次迭代中,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(); 解决这两个问题,但想了解为什么第二个示例会引发错误而第一个不会)

【问题讨论】:

    标签: c# linq foreach


    【解决方案1】:

    为什么第一个例子也没有爆炸?

    tableLayoutPanel1.Controls 中的IEnumerator 实现没有执行正确的检查以查看集合是否已被修改。这并不意味着它是一个安全的操作(因为结果不合适),而是该类没有以引发异常的方式实现(它可能应该)。

    通过List&lt;T&gt; 枚举并删除项目时引发的异常实际上是由List&lt;T&gt;.Enumerator 类型引发的,并且不是“通用”语言功能。

    请注意,这是List&lt;T&gt;.Enumerator 类型的不错的功能,正如IEnumerator&lt;T&gt; 文档明确指出的那样:

    只要集合保持不变,枚举数就保持有效。如果对集合进行了更改,例如添加、修改或删除元素,则枚举器将不可恢复地失效,并且其行为未定义。

    虽然没有合同要求抛出异常,但当您进入“未定义行为”领域时,这样做对实现是有益的。

    在这种情况下,TableLayoutControlCollection 类枚举器实现不会执行额外的检查,因此您会看到当您使用“行为未定义”的功能时会发生什么。 (在这种情况下,它会删除所有其他控件。)

    【讨论】:

    • 这个问题的原因是否与Enumerable.OfType是通过延迟执行实现的,因此每次枚举时序列总是变化的事实无关?
    • @TimSchmelter 好吧,OfType&lt;T&gt; 只是通过直接使用源枚举器进行枚举 - 虽然它不进行检查,但如果您枚举对象并将检查放入循环中,也会发生同样的事情。底层枚举器没有检查控件集合是否被修改。
    • @TimSchmelter:我怀疑所有 LINQ 操作都可以从原始邮政编码中删除,并且它的行为方式完全相同。正如 Reed 所说,这是枚举器实现的结果。 OfType 与它无关。
    • 太棒了,谢谢。所以在我的第一种情况下,linq 表达式是否在 foreach 循环的每次迭代中都被重新评估?还是 linq 从 tablelayoutpanel.controls 集合中获取 IEnumerator?
    • @John 它没有被重新评估。 OfType&lt;T&gt;tableLayoutPanel.Controls 获取 IEnumerator,并将其包装在自己的枚举器中,然后在 foreach 循环期间枚举一次
    【解决方案2】:

    所以在我的第一种情况下,linq 表达式在每个 foreach循环的迭代?或者是 linq 从 tablelayoutpanel.controls 集合?

    把它想象成是这样发生的,这两种情况都发生了:

        foreach (string l in (from letter in stringList select letter)) 
        { 
            stringList.Remove(l); 
        } 
    

    上面的伪代码说明,当您尝试删除一个项目时,您实际上是在枚举stringList 集合的中间。 Reed 的意思是,在第二种情况下,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); 
        } 
    

    【讨论】:

    • 谢谢布拉德。我现在明白了。 @每个回复的人:谢谢,这真的让我在脑海中清除了这个主题。你这家伙的摇滚!
    【解决方案3】:

    试试这个:

    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);
            }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-28
      • 1970-01-01
      相关资源
      最近更新 更多