LINQ to XML 包含各种方法,可用于直接修改 XML 树。 可以添加元素、删除元素、更改元素的内容、添加属性等。 修改 XML 树中描述了此编程接口。 如果循环访问一个轴,例如 Elements,而在循环访问该轴时正在修改 XML 树,那么可能会发生一些异常问题。
此问题有时称为“万圣节问题”。
使用循环访问集合的 LINQ 编写一些代码时,将使用声明性样式编写代码。 更像是在描述你想要 什么 ,而不是你想要 如何 完成它。 如果编写代码,1) 获取第一个元素,2) 测试它是否有某些条件,3) 修改它,4) 将其放回列表中,则这将是命令性代码。 你告诉计算机 如何 去做你想做的事情。
在同一作中混合这些代码样式会导致问题。 请考虑以下事项:
假设你有一个链接列表,其中包含三个项目(a、b 和 c):
a -> b -> c
现在,假设你想要在链接列表中移动,并添加三个新项(a'、b'和 c')。 希望生成的链接列表如下所示:
a - a' -> b ->> b' -> c '>
因此,编写代码遍历列表,并在每个项后面添加一个新项。 会发生什么情况是代码将首先看到 a
元素,并在元素之后插入 a'
。 现在,代码将移动到列表中的下一个节点,即现在位于 a'
,因此它会在 a' 和 b 之间添加一个新项到列表中!
如何解决此问题? 嗯,你可以创建原始链接列表的副本,并创建一个全新的列表。 或者,如果你正在编写纯命令性代码,你可能会找到第一个项目,添加新项,然后在链接列表中前进两次,向前推进刚刚添加的元素。
示例:添加 while 循环访问
例如,假设你想要编写代码来创建树中每个元素的副本:
XElement root = new XElement("Root",
new XElement("A", "1"),
new XElement("B", "2"),
new XElement("C", "3")
);
foreach (XElement e in root.Elements())
root.Add(new XElement(e.Name, (string)e));
Dim root As XElement = _
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
For Each e As XElement In root.Elements()
root.Add(New XElement(e.Name, e.Value))
Next
此代码进入无限循环。 该 foreach
语句循环遍历 Elements()
轴,并向 doc
元素添加新元素。 结果它也循环访问刚添加的元素。 由于它分配了循环每次迭代的新对象,因此它最终会消耗所有可用的内存。
可以使用标准查询运算符将集合拉取到内存 ToList 中来解决此问题,如下所示:
XElement root = new XElement("Root",
new XElement("A", "1"),
new XElement("B", "2"),
new XElement("C", "3")
);
foreach (XElement e in root.Elements().ToList())
root.Add(new XElement(e.Name, (string)e));
Console.WriteLine(root);
Dim root As XElement = _
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
For Each e As XElement In root.Elements().ToList()
root.Add(New XElement(e.Name, e.Value))
Next
Console.WriteLine(root)
现在代码正常工作。 生成的 XML 树如下:
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
示例:在迭代过程中删除
如果想要在特定级别删除所有节点,则可能希望编写如下所示的代码:
XElement root = new XElement("Root",
new XElement("A", "1"),
new XElement("B", "2"),
new XElement("C", "3")
);
foreach (XElement e in root.Elements())
e.Remove();
Console.WriteLine(root);
Dim root As XElement = _
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
For Each e As XElement In root.Elements()
e.Remove()
Next
Console.WriteLine(root)
但是,这并没有达到你想要的效果。 在这种情况下,删除第一个元素后,A 将从根中包含的 XML 树中删除,并且执行迭代的 Elements 方法中的代码找不到下一个元素。
此示例生成以下输出:
<Root>
<B>2</B>
<C>3</C>
</Root>
解决方案是再次调用ToList来实现集合,如下所示:
XElement root = new XElement("Root",
new XElement("A", "1"),
new XElement("B", "2"),
new XElement("C", "3")
);
foreach (XElement e in root.Elements().ToList())
e.Remove();
Console.WriteLine(root);
Dim root As XElement = _
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
For Each e As XElement In root.Elements().ToList()
e.Remove()
Next
Console.WriteLine(root)
此示例生成以下输出:
<Root />
或者,可以通过调用 RemoveAll 父元素来完全消除迭代:
XElement root = new XElement("Root",
new XElement("A", "1"),
new XElement("B", "2"),
new XElement("C", "3")
);
root.RemoveAll();
Console.WriteLine(root);
Dim root As XElement = _
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
root.RemoveAll()
Console.WriteLine(root)
示例:为什么 LINQ 无法自动处理这些问题
一种方法是总是将所有内容放入内存,而不是执行迟缓计算。 但是,在性能和内存使用方面,这非常昂贵。 事实上,如果 LINQ 和 LINQ to XML 采用此方法,那么在实际情况下会失败。
另一种可能的方法是将某种事务语法放入 LINQ,并让编译器尝试分析代码以确定是否需要具体化任何特定集合。 但是,尝试确定具有副作用的所有代码是非常复杂的。 请考虑以下代码:
var z =
from e in root.Elements()
where TestSomeCondition(e)
select DoMyProjection(e);
Dim z = _
From e In root.Elements() _
Where (TestSomeCondition(e)) _
Select DoMyProjection(e)
此类分析代码需要分析 TestSomeCondition 和 DoMyProjection 的方法以及这些方法调用的所有方法,以确定任何代码是否有副作用。 但是,分析代码不能只查找任何具有副作用的代码。 在此情况下,它需要选择只对 root
的子元素具有副作用的代码。
LINQ to XML 不会尝试执行任何此类分析。 由你来避免这些问题。
示例:使用声明性代码生成新的 XML 树,而不是修改现有树
为了避免此类问题,请不要混合声明性代码和命令性代码,即使你确切地知道集合的语义和修改 XML 树的方法的语义。 如果你编写代码时避免潜在问题,将来其他开发人员可能需要维护你的代码,而他们可能不太了解这些问题。 如果混合声明性编码和命令性编码样式,则代码将更加脆弱。 如果编写代码来具体化集合以避免这些问题,请在代码中适当地添加注释,以便维护程序员理解问题。
如果性能和其他注意事项允许,则仅使用声明性代码。 不要修改现有的 XML 树。 相反,请生成一个新示例,如以下示例所示:
XElement root = new XElement("Root",
new XElement("A", "1"),
new XElement("B", "2"),
new XElement("C", "3")
);
XElement newRoot = new XElement("Root",
root.Elements(),
root.Elements()
);
Console.WriteLine(newRoot);
Dim root As XElement = _
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
Dim newRoot As XElement = New XElement("Root", _
root.Elements(), root.Elements())
Console.WriteLine(newRoot)