Search code examples
c#linq-to-xmlmorelinq

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?


Solution

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