Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
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 -> c'
Поэтому вы пишете код, который выполняет итерацию по списку, и для каждого элемента добавляет новый элемент сразу после него. В этом случае ваш код сначала увидит элемент a и вставит a' после него. Теперь код перейдет к следующему узлу в списке, который теперь a', поэтому он добавляет новый элемент между a и b в список!
Как вы решите это? Ну, вы можете создать копию исходного связанного списка и создать совершенно новый список. Или если вы пишете чисто императивный код, вы можете найти первый элемент, добавить новый элемент, а затем дважды перейти в связанный список, продвигаясь по элементу, который вы только что добавили.
Пример. Добавление при итерации
Например, предположим, что вы хотите написать код для создания дубликата каждого элемента в дереве:
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)