【问题标题】:Why the same LINQ expression behaves differently in two different foreach loops?为什么相同的 LINQ 表达式在两个不同的 foreach 循环中表现不同?
【发布时间】:2013-03-28 12:09:10
【问题描述】:

我有以下 XML。

<Parts>
  <Part name="Part1" disabled="true"></Part>
  <Part name="Part2" disabled="false"></Part>
  <Part name="Part3" ></Part>
  <Part name="Part4" disabled="true"></Part>  
</Parts>

我想删除disabled 属性设置为true 的节点。如果任何 'Part' 元素都没有使用 'disabled' 属性,则表示它没有被禁用。

我写了以下代码:

XmlNode root = xmlDoc.DocumentElement;
List<XmlNode> disabledNodes = new List<XmlNode>();
foreach(XmlNode node in root.ChildNodes)
{
    if(node.Attributes["disabled"] != null && 
        Convert.ToBoolean(node.Attributes["disabled"].Value))
    {
        disabledNodes.Add(node);
    }
}

foreach (XmlNode node in disabledNodes)
{
    root .RemoveChild(node);
}

此代码按预期从 XML 中删除 2 个节点。

然后我编写了以下代码以使代码紧凑:

foreach (XmlNode node in root.ChildNodes.Cast<XmlNode>()
    .Where(child => child.Attributes["disabled"] != null && 
    Convert.ToBoolean(child.Attributes["disabled"].Value)))
{
    root.RemoveChild(node); // This line works fine without any exception.
}

我发现这个循环只迭代了一次,从 XML 中只删除了一个节点。


编辑问题:

现在当我更改foreach 循环时,这次我使用ToList() 方法将LINQ 表达式的结果转换为List&lt;T&gt;(正如@Toni Petrina 在他的回答中所建议的那样)。这一次效果很好!

 foreach (XmlNode node in root.ChildNodes.Cast<XmlNode>()
        .Where(child => child.Attributes["disabled"] != null && 
        Convert.ToBoolean(child.Attributes["disabled"].Value)).ToList())
    {
        root.RemoveChild(node); // This line works fine without any exception.
    }

为什么使用ToList() 使LINQ 表达式在foreach 循环中按预期工作? LINQ 结果在两种不同情况下表现不同的任何技术原因?

我正在使用 .NET 4.0。

【问题讨论】:

  • 我认为问题在于您正在迭代“root”,同时尝试从中删除一些 foreach 循环不允许的内容
  • @CSharpLearner 从foreach 的正文中删除RemoveChild() 代码,看看它迭代了多少次?
  • 我在这里尝试了这段代码:ideone.com/7gyQfj 并得到了我们期望的错误(但你不是)。有趣的是,我在 VSExpress 2012 上进行了尝试,它按预期工作(删除了所有预期的节点)。
  • 不再是同一个查询。当您从 LINQ 迭代一个集合时,您一次请求一个项目。并且您更改了从中提取项目的集合。如果您不修改集合,它会正常工作。请记住:在迭代集合时不要更改集合。
  • 我已经发布了一个关于为什么在某些情况下您看不到异常的问题。 stackoverflow.com/questions/15682613/…

标签: c# .net linq linq-to-xml


【解决方案1】:

您的问题是您在枚举时更改了集合。这是错误的。你应该使用这样的东西:

var disabledNodes = root.ChildNodes.Cast<XmlNode>()
    .Where(child => child.Attributes["disabled"] != null && 
    Convert.ToBoolean(child.Attributes["disabled"].Value)).ToArray();

foreach (XmlNode node in disabledNodes)
{
    root.RemoveChild(node);
}

更新

这是由于延迟执行。如果您不使用 ToArray() 或 ToList(),IEnumerator 在您需要下一个元素时(即当 foreach 进入下一回合时)一个一个地返回值。当 foreach 执行第一轮时,您的源代码会更改并且迭代停止。但是如果你调用 ToArray(),你会得到一个包含 disabledNodes 数组的新变量,foreach 不会改变它迭代的集合。

【讨论】:

  • 我知道这是可以做到的。但是我的问题是'为什么直接在代码中编写 LINQ 表达式时它不起作用'?
  • 目前所写的内容不会改变任何内容。您需要添加 .ToList 或其他内容,否则您在删除时仍在迭代基础列表。
  • 因为我的回答其实是正确的。使用 LINQ 完成的所有操作都会在请求时执行,而不是在语句结束时执行。
  • 因为第一次执行后你的源代码被改变并且迭代停止了
  • @George,你说得对,应该调用 ToArray() 或 ToList()。
【解决方案2】:

写:

foreach (XmlNode node in root.ChildNodes.Cast<XmlNode>()
    .Where(child => child.Attributes["disabled"] != null && 
    Convert.ToBoolean(child.Attributes["disabled"].Value)).ToList())
{
    root.RemoveChild(node);
}

我添加了额外的 ToList() 来强制立即执行 LINQ 表达式。

当您创建 LINQ 查询时,您会得到一个 IEnumerable 集合,该集合实际上并不包含任何结果。即使您编写了所有这些 Select 和 Where 以及许多其他子句,完整的查询也不会在您开始迭代之前执行。只有这样才能运行实际的查询。

在原始代码中,您创建了一个查询并开始对其进行迭代。您收到了通过所有 LINQ 子句的第一个项目并删除了第一个节点。但是由于您正在迭代现在已修改的根集合,因此迭代停止。

您不能在 foreach 循环的主体中更改您正在迭代的集合。

【讨论】:

  • 遍历IEnumerable 将执行它。 OP 编写的代码应该迭代与您编写的代码相同的元素。
  • 这实际上可能会解决问题(但出于其他原因)。见this comment
  • @LukeHennerley 这可能并不完全正确。由于正在迭代的集合实际上在 foreach 主体中发生了变化,因此在迭代之前强制执行将防止此问题。
  • @LukeHennerley 所以我经历了实际编译代码的麻烦并且它可以工作,谁会想到:/
  • @Toni Petrina:当我在foreach 循环中迭代LINQ 表达式时,我不是在“使用”返回的集合吗?如果是这种情况,为什么它还要迭代一次?我发现您将其转换为 List 的建议实际上解决了这个问题。但我想知道为什么它会这样工作。所以我也编辑了我原来的问题。
猜你喜欢
  • 2016-05-12
  • 2016-06-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多