why does i.Parent.ReplaceWith(i) not throw an exception?

In these statements (running with MoreLinq):

var xml = @"
<div>
<p>
    <h2>hey</h2>
</p>
<pre />
<h2 class=""cool"" />
<p>
    <h2>okay</h2>
</p>
</div>
".Trim();

var div = XElement.Parse(xml);
var h2Elements = div.Descendants("h2");
h2Elements.ToList().ForEach(i =>
{
    if(i.Parent.Name != "p") return;
    i.Parent.ReplaceWith(i);
});

I see that i.Parent.ReplaceWith(i) does not throw an exception but this will throw a null-reference exception (using ForEach from MoreLinq):

h2Elements.ForEach(i =>
{
    if(i.Parent.Name != "p") return;
    i.Parent.ReplaceWith(i);
});

I understand that LINQ's ToList() is making a copy of the list but would not the copy just throw an exception as well? Also, is there a memory leak happening here with some kind of orphaned references?

Jon Skeet
people
quotationmark

You don't need MoreLINQ to demonstrate this at all - and you can simplify the sample code, too:

using System;
using System.Linq;
using System.Xml.Linq;

class Program
{    
    static void Main()
    {
        var element = new XElement(
            "root",
            new XElement("parent", new XElement("child")),
            new XElement("parent", new XElement("child"))
        );
        var children = element.Descendants("child");
        foreach (var child in children.ToList())
        {
            child.Parent.ReplaceWith(child);
        }
    }
}

Without the ToList call, a NullReferenceException is thrown. With the ToList() call, there's no exception. The exception is:

Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Xml.Linq.XContainer.<GetDescendants>d__39.MoveNext()
   at Program.Main()

Basically, you're invalidating the query by modifying the tree while iterating over it. This is a bit like calling Add or Remove on a List<T> while iterating over it, but it's harder for LINQ to XML to spot the problem and throw a meaningful exception. It's important to note that the exception doesn't come when calling ReplaceWith - it's the iteration part that's failing, as it can't find traverse the tree properly after you've modified it.

When you call ToList(), you're just getting separate XElement values in a list - when you iterate over that list, any changes to the elements won't change the references that appear in the list.

As for a memory leak: nope, that's what the garbage collector is for...

people

See more on this question at Stackoverflow