为什么修改XML文件后XmlNodeList.Countreturn结果不一致?

Why does XmlNodeList.Count return inconsistent results after modifying the XML document?

我有一个 XML 文档,我试图将节点彼此分开。我只想要根节点,然后是第二个节点,然后是第二个节点中存在的节点列表。我有 运行 问题,当我从第二个节点或主节点删除节点时,我的列表变为空。我不明白为什么会这样,尤其是因为下面这种奇怪的行为。

class Program
{
    static void Main(string[] args)
    {
        XmlDocument doc = new XmlDocument();

        doc.Load(@"C:\Users\Username\Desktop\Diagram.xml");

        XmlNode rootNode = doc.DocumentElement;
        XmlNode secondNode = doc.SelectSingleNode(rootNode.Name + "/root");

        XmlNodeList nodelist = doc.SelectNodes("//root/mxCell");

        Console.WriteLine("-----------------------------------------");
        Console.WriteLine(RemoveChildren(rootNode).OuterXml);
        Console.WriteLine("-----------------------------------------");
        Console.WriteLine(RemoveChildren(secondNode).OuterXml);
        Console.WriteLine("-----------------------------------------");
        //Console.WriteLine(rootNode.OuterXml);
        Console.WriteLine(nodelist.Count); //Becomes 0
        if (nodelist != null && nodelist.Count > 0)
        {
            foreach (XmlNode n in nodelist)
            {
                Console.WriteLine(n.OuterXml);
            }
        }

        Console.ReadLine();
    }

    private static XmlNode RemoveChildren(XmlNode n) {

        while (n.FirstChild != null)
        {
            n.RemoveChild(n.FirstChild);
        }

        return n;
    }
}

如果我运行这段代码,我的nodelist.count会变成0,为什么nodelist变成了0,但是为什么还能访问到第二个节点?

但是,如果我在 doc.SelectNodes("//root/mxCell") 之后添加 foreach 循环;计数将变为 4。

像这样,

class Program
{
    static void Main(string[] args)
    {
        XmlDocument doc = new XmlDocument();

        doc.Load(@"C:\Users\Username\Desktop\Diagram.xml");

        XmlNode rootNode = doc.DocumentElement;
        XmlNode secondNode = doc.SelectSingleNode(rootNode.Name + "/root");

        XmlNodeList nodelist = doc.SelectNodes("//root/mxCell");

        // Added code here
        if (nodelist != null && nodelist.Count > 0)
        {
            foreach (XmlNode n in nodelist)
            {
                Console.WriteLine(n.OuterXml);
            }
        }
        // End of added code

        Console.WriteLine("-----------------------------------------");
        Console.WriteLine(RemoveChildren(rootNode).OuterXml);
        Console.WriteLine("-----------------------------------------");
        Console.WriteLine(RemoveChildren(secondNode).OuterXml);
        Console.WriteLine("-----------------------------------------");
        //Console.WriteLine(rootNode.OuterXml);
        Console.WriteLine(nodelist.Count); //Becomes 4
        if (nodelist != null && nodelist.Count > 0)
        {
            foreach (XmlNode n in nodelist)
            {
                Console.WriteLine(n.OuterXml);
            }
        }

        Console.ReadLine();
    }

    private static XmlNode RemoveChildren(XmlNode n) {

        while (n.FirstChild != null)
        {
            n.RemoveChild(n.FirstChild);
        }

        return n;
    }
}

计数现在是 4。

这里是xml使用的:

<mxGraphModel dx="1086" dy="596" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
  <root>
    <mxCell id="0"/>
    <mxCell id="1" parent="0"/>
    <mxCell id="YJb7HCrh72y2aGPrfETQ-1" value="" style="endArrow=classic;html=1;" parent="1" edge="1">
      <mxGeometry width="50" height="50" relative="1" as="geometry">
        <mxPoint x="130" y="310" as="sourcePoint"/>
        <mxPoint x="180" y="260" as="targetPoint"/>
      </mxGeometry>
    </mxCell>
    <mxCell id="YJb7HCrh72y2aGPrfETQ-2" value="" style="endArrow=classic;html=1;" parent="1" edge="1">
      <mxGeometry width="50" height="50" relative="1" as="geometry">
        <mxPoint x="290" y="270" as="sourcePoint"/>
        <mxPoint x="340" y="220" as="targetPoint"/>
      </mxGeometry>
    </mxCell>
  </root>
</mxGraphModel>

您观察到的行为可以更简单地演示如下。下面的单元测试会成功(demo fiddle here):

XmlNodeList nodelist = doc.SelectNodes("*/*"); // Select all children of the root node
RemoveChildren(doc.DocumentElement);
Assert.IsTrue(nodelist.Count == 0); // Passes

虽然以下将失败:

XmlNodeList nodelist = doc.SelectNodes("*/*"); // Select all children of the root node
Assert.IsTrue(nodelist.Count > 0);  // Passes
RemoveChildren(doc.DocumentElement);
Assert.IsTrue(nodelist.Count == 0); // FAILS!?

为什么在删除某些节点之前仅仅添加nodelist.Count会导致删除节点后nodelist的内容不一致?

事实证明,在这种情况下 SelectNodes() 返回的 XmlNodeList 没有指定的行为。 来自 documentation remarks for XmlNode.SelectNodes() :

The XmlNodeList object returned by this method will be valid while the underlying document remains unchanged. If the underlying document changes, unexpected results may be returned (no exception will be thrown).

这样的"unexpected results"就是你观察到的。一旦 RemoveChildren() 被调用,.Net 不指定或保证 nodelist 的内容。 (实际上,返回的 XmlNodeList 似乎使用了 惰性求值 机制。一旦对节点列表进行计数或迭代(但不是之前),XPath 查询被评估一次且仅一次,结果被缓存并随后重用。)

此记录的对 XPath 查询返回的节点列表的限制与 XmlNodeList 的一般文档形成对比,后者指出:

Changes to the children of the node object that the XmlNodeList collection was created from are immediately reflected in the nodes returned by the XmlNodeList properties and methods.

该评论似乎仅适用于 XmlNode DOM 个对象的方法返回的 XmlNodeList 个子列表。

为了避免不一致,你可以在求值后立即将 XmlNodeList 显式具体化为 List<XmlNode>,如下所示:

var nodelist = doc.SelectNodes("*/*").Cast<XmlNode>().ToList();

演示 fiddle here.

切换到 LINQ to XML 将是另一种选择,因为它通常比旧的 XmlDocument API.

更容易使用