2

如果有人能解释这一点,我会感到惊讶,但知道其他人是否可以重现我正在经历的怪异会很有趣......

我们有一个基于处理大量表单的 InfoPath 的东西。表单数据应该符合 XSD,但 InfoPath 不断以所谓的“我的字段”的形式添加自己的元数据。我们想删除我的字段,我写了这个简单的方法:

string StripMyFields(string xml)
{
    var doc = new XmlDocument();
    doc.LoadXml(xml);

    var matches = doc.SelectNodes("//node()").Cast<XmlNode>().Where(n => n.NamespaceURI.StartsWith("http://schemas.microsoft.com/office/infopath/"));
    Dbug("Found {0} nodes to remove.", matches.Count());
    foreach (var m in matches)
        m.ParentNode.RemoveChild(m);

    return doc.OuterXml;
}

现在来了真正奇怪的东西!当我运行此代码时,它的行为与我预期的一样,删除了 InfoPath 命名空间中的所有节点。但是,如果我注释掉对 Dbug 的调用,则代码完成,但 XML 中仍保留一个“我的字段”。

我什至注释掉了方便的 Dbug 方法的内容,它的行为仍然相同:

void Dbug(string s, params object[] args)
{
    //if (args.Length > 0)
    //    s = string.Format(s, args);
    //Debug.WriteLine(s);
}

输入 XML:

<?xml version="1.0" encoding="UTF-8"?>
<skjema xmlns:my="http://schemas.microsoft.com/office/infopath/2003/myXSD/2008-03-03T22:25:25" xml:lang="en-us">
    <Field-1643 orid="1643">data.</Field-1643>
    <my:myFields>
        <my:field1>Al</my:field1>
        <my:group1>
            <my:group2>
                <my:field2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2009-01-01</my:field2>
                <Field-1611 orid="1611">More data.</Field-1611>
                <my:field3>true</my:field3>
            </my:group2>
            <my:group2>
                <my:field2>2009-01-31</my:field2>
                <my:field3>false</my:field3>
            </my:group2>
        </my:group1>
    </my:myFields>
    <Field-1612 orid="1612">Even more data.</Field-1612>
    <my:field3>Blah blah</my:field3>
</skjema>

除非我调用 Dbug,否则不会删除“my:field3”元素(在底部,文本“Blah blah”)。

显然宇宙不应该是这样的,但我很想知道其他人是否能够复制。

我在 Win8 Enterprise 6.2.9200 上使用 VS2012 Premium (11.0.50727.1 RTMREL) 和 FW 4.5.50709。

4

4 回答 4

3

第一件事。LINQ 使用称为延迟执行的概念。这意味着在您实际实现查询(例如通过枚举)之前不会获取任何结果。

为什么这对您的节点删除问题很重要?让我们看看您的代码中发生了什么:

  1. SelectNodes创建XPathNodeIterator,用于XPathNavigator提供数据以XmlNodeList返回SelectNodes
  2. XPathNodeIterator基于提供的 XPath 表达式遍历 xml 文档树
  3. CastWhere简单地决定返回的节点是否应该XPathNodeIterator参与最终结果

我们在DBug方法调用之前到达。暂时假设它不存在。在这一点上,实际上还没有发生任何事情。我们只得到了未实现的 LINQ查询。

当我们开始迭代时,情况会发生变化。所有的迭代器(CastWhere有自己的迭代器)开始滚动。WhereIterator询问CastIterator项目,然后询问XPathNodeIterator哪个最终返回第一个节点(Field-1643)。不幸的是,这个Where测试失败了,所以我们要求下一个。更幸运的是my:myFields,这是一场比赛 - 我们将其删除。

我们快速继续my:field1(同样,WhereIteratorCastIteratorXPathNodeIterator),它也被删除了。停在这里片刻。删除my:field1会将其与其父级分离,这会导致将其 ( my:field1) 兄弟姐妹设置为null(在删除节点之前/之后没有其他节点)。

事情的现状是什么?XPathNodeIterator知道它的当前元素是my:field1刚刚被删除的节点。与 parent 分离一样被删除,但迭代器仍然持有引用。听起来不错,让我们请求下一个节点。有什么XPathNodeIterator作用?检查它的Current项目,并要求NextSibling(因为它没有孩子先走)——也就是说null,假设我们刚刚执行了分离。这意味着迭代已经结束。任务完成。

结果,通过在迭代期间更改集合结构,您只从文档中删除了两个节点(而实际上只有一个,因为第二个删除的节点是已删除的节点的子节点)。

使用更简单的 XML 可以观察到相同的行为:

<Root>
    <James>Bond</James>
    <Jason>Bourne</Jason>
    <Jimmy>Keen</Jimmy>
    <Tom />
    <Bob />
</Root>

假设我们想要删除以 开头的节点J,从而生成只包含诚实人名的文档:

var doc = new XmlDocument();
doc.LoadXml(xml);

var matches = doc
    .SelectNodes("//node()")
    .Cast<XmlNode>()
    .Where(n => n.Name.StartsWith("J"));

foreach (var node in matches)
{
    node.ParentNode.RemoveChild(node);
}

Console.WriteLine(doc.InnerXml);

不幸的是,杰森吉米留下了。James的下一个兄弟姐妹(由迭代器返回的那个)原本应该是Jason,但是一旦我们将James从树中分离出来,就没有兄弟姐妹并且迭代结束。

现在,为什么它适用于DBugCount调用具体化查询。迭代器已经运行,当我们开始循环时,我们可以访问我们需要的所有节点。ToList在调用之后Where或在调试期间检查结果时会发生同样的事情(VS 甚至通知您检查结果将枚举集合)。

于 2013-07-08T23:09:24.460 回答
0

Very strange, its only when you actually view the results while debugging that it removes the last node. Incidentally, converting the result to a List and then looping through it also works.

List<XmlNode> matches = doc.SelectNodes("//node()").Cast<XmlNode>().Where(n =>   n.NamespaceURI.StartsWith("http://schemas.microsoft.com/office/infopath/")).ToList();
        foreach (var m in matches)
        {
            m.ParentNode.RemoveChild(m);
        }
于 2013-07-08T14:26:08.033 回答
0

我认为这归结于薛定谔的猫问题,在您查看或采取行动之前,Where 不会实际编译查询结果。意思是,在您调用 Count() (或任何其他用于获取结果的函数)或在调试器中查看它之前,结果不存在。作为测试,请尝试这样放置:

if (matches.Any())
    foreach (var m in matches)
        m.ParentNode.RemoveChild(m);
于 2013-07-08T14:16:27.657 回答
0

jimmy_keen 的解决方案对我有用。我只有一个简单的

//d is an XmlDocument
XmlNodeList t = d.SelectNodes(xpath);
foreach (XmlNode x in t)
{
    x.ParentNode.RemoveChild(x);
}
d.Save(outputpath);

这将仅删除 3 个节点,而在调试模式下单步执行将删除 1000 多个节点。

只需在 foreach 解决问题之前添加一个 Count :

var count = t.Count;

于 2016-11-08T13:28:10.263 回答