Смешанные декларативные и императивные ошибки кода (LINQ to XML)
LINQ to XML содержит различные методы, позволяющие напрямую изменять дерево XML. Можно добавить элементы, удалить элементы, изменить содержимое элемента, добавить атрибуты и т. п. Этот интерфейс программирования описан в разделе "Изменение деревьев XML". Если вы выполняете итерацию по одной из осей, например Elements, и вы изменяете XML-дерево при итерации по оси, вы можете в конечном итоге получить некоторые странные ошибки.
Этот вид ошибки иногда называется Halloween Problem.
При написании кода с помощью LINQ, который выполняет итерацию по коллекции, вы пишете код в декларативном стиле. Это больше похоже на описание того, что вы хотите, а именно то, как вы хотите сделать это. Если написать код, при котором 1) извлекается первый элемент, 2) выполняется его проверка согласно определенному условию, 3) выполняется его изменение и 4) выполняется его помещение назад в список элементов, то это означает, что это был бы императивный код. Вы говорите компьютеру , как сделать то, что вы хотите сделать.
Смешение этих стилей кода в одной операции является источником неполадок. Рассмотрим следующий пример.
Предположим, дан список из трех элементов (a, b и c):
a -> b -> c
Теперь предположим, что необходимо пройти по связанному списку, добавив три новых пункта (a', b' и c'). При этом необходимо, чтобы результирующий список выглядел так:
a -> a -> b -> 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)