简体   繁体   中英

Why the same LINQ expression behaves differently in two different foreach loops?

I have following 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>

I want to remove the nodes for which disabled attribute is set to true . If 'disabled' attribute is not used for any 'Part' element, it means it's not disabled.

I wrote following code:

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

This code removes 2 nodes from the XML as expected.

I then wrote following code to make code compact:

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.
}

I found that this loop iterate only once, removing only one node from the XML.


EDITED QUESTION:

Now when I change the foreach loop, this time I convert the result of LINQ expression to the List<T> using ToList() method (as suggested by @Toni Petrina in his answer). And this time it works fine !

 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.
    }

Why the use of ToList() made LINQ expression work in foreach loop as expected? Any technical reason why result of LINQ behaves differently in two different situations?

I am using .NET 4.0.

Your problem is that you change collection when enumerate it. It is wrong. You should use something like this:

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

Update

It is due to deffered execution. If you do not use ToArray() or ToList(), IEnumerator returns value one by one when you need next element (ie when foreach go to next turn). And when foreach execute first turn, your source become changed and iteration stopped. But if you call ToArray(), you get new variable that contains array of disabledNodes and foreach will not change collection that it iterates.

Write:

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

I've added extra ToList() to force immediate execution of the LINQ expression.

When you create a LINQ query, you get an IEnumerable collection which doesn't actually hold any results. Even though you wrote all those Select and Where and many other clause, the complete query isn't executed before you start iterating over it. Only then is the actually query run.

In the original code, you created a query and started iterating over it. You received the first item that passes through all LINQ clauses and remove the first node. But since you were iterating over root collection which is now modified, the iteration stops.

You cannot change the collection you are iterating over in the body of foreach loop.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM