Объектная модель LINQ to XML
49LINQ --- LINQ to XML --- Объектная модель LINQ to XML
С новым API-интерфейсом LINQ to XML пришла новая объектная модель, содержащая множество новых классов, находящихся в пространстве имен System.Xml.Linq. Один из них — статический класс, в котором находятся расширяющие методы LINQ to XML; два класса компараторов — XNodeDocumentOrderComparer и XNodeEqualityComparer; остальные классы используются для построения деревьев XML. Эти остальные классы показаны на диаграмме, представленной на рисунке:
Следует отметить некоторые интересные вещи:
Три из этих классов, XObject, XContainer и XNode, являются абстрактными, так что конструировать их объекты никогда не придется.
Атрибут XAttribute не наследуется от узла XNode. Фактически, это вообще не узел, а совершенно другого типа класс, который на самом деле представляет пару "ключ-значение".
Потоковые элементы XStreamingElement не имеют отношения наследования с элементом XElement.
Классы XDocument и XElement — единственные классы, имеющие узлы, унаследованные от XNode.
Все эти классы используются для построения деревьев XML. Самое примечательное состоит в том, что применяется класс XElement, поскольку, как уже упоминалось, в LINQ to XML центральную роль играет элемент, в противоположность документу в W3C XML DOM.
Отложенное выполнение запросов, удаление узлов и "проблема Хэллоуина"
Этот раздел служит предупреждением, чтобы вы опасались некоторых подводных камней. Первый из них — отложенное выполнение запросов. Никогда не забывайте, что многие из операций LINQ откладывают выполнение запроса до тех пор, пока это не станет абсолютно необходимо, и это может вызвать потенциальные побочные эффекты.
Другая проблема — так называемая проблема Хэллоуина (Halloween problem), которая получила свое название потому, что впервые всплыла в дискуссии внутри небольшой группы экспертов именно в день этого праздника. К проблемам этого типа можно отнести почти любую проблему, которая проявляется при изменении тех данных в процессе итерации, которые затрагивают эту итерацию.
Впервые она была обнаружена инженерами баз данных при работе над оптимизатором базы данных. Они столкнулись с ней, когда тестировали запрос, изменяющий значение столбца базы данных, который разрабатываемый оптимизатор использовал в качестве индекса. Их тестовый запрос должен был извлечь запись на основе индекса, созданного на одном из столбцов таблицы, и этот запрос должен был изменить значение этого столбца. Поскольку затронутый столбец определяет индексацию записи, эта запись затем появлялась дальше в списке записей, извлеченная тем же запросом и подвергаясь повторной обработке. Это приводило к бесконечному циклу, потому что при всяком извлечении ее из набора записей она обновлялась и перемещалась далее в наборе записей, где вновь извлекалась и обрабатывалась, и так до бесконечности.
Вы сами наверняка сталкивались с "проблемой Хэллоуина", даже не зная, что она так называется. Приходилось ли вам когда-либо работать с какой-нибудь коллекцией, выполняя итерацию по ней и удаляя элемент, что приводило к прерыванию итерации или к ее неправильному поведению? Например, возьмем крупный комплект серверных элементов управления ASP.NET. Этот комплект включает серверный элемент DataGrid, и нужно выборочно удалить из него записи. При итерации по записям с начала и до конца соответствующие записи удалялись, но при этом терялась текущая точка итерации. В результате оставались записи, которые должны были быть удалены, и удалялись те, которые не должны были. Проблема решается выполнением итерации в обратном порядке.
В LINQ to XML с этой проблемой, скорее всего, придется столкнуться при удалении узлов из дерева XML, хотя это может произойти в других случаях, так что следует иметь это в виду при кодировании. Рассмотрим пример, представленный ниже:
XDocument xDocument =
new XDocument(
new XElement("Employees",
new XElement("Employee",
new XAttribute("type", "Programmer"),
new XElement("FirstName", "Alex"),
new XElement("LastName", "Erohin")),
new XElement("Employee",
new XAttribute("type", "Editor"),
new XElement("FirstName", "Elena"),
new XElement("LastName", "Volkova"))));
IEnumerable<XElement> elements =
xDocument.Element("Employees").Elements("Employee");
foreach (XElement x in elements)
Console.WriteLine("Исходный элемент: {0} : значение = {1}",
x.Name, x.Value);
foreach (XElement x in elements)
{
Console.WriteLine("Удаление: {0} = {1}", x.Name, x.Value);
x.Remove();
}
Console.WriteLine("\n" + xDocument);
В этом коде сначала строится документ XML. Затем создается последовательность элементов Employee. По этой последовательности будет выполняться перечисление с удалением элементов. Затем отображаются элементы в последовательности, так что можно удостовериться, что в действительности есть два элемента Employee. Далее вновь выполняется перечисление последовательности с отображением сообщения об удалении элемента и собственно удаление элемента Employee. В конце отображается результирующий документ XML.
Если "проблема Хэллоуина" не проявится, сообщение "Удаление..." должно быть выведено дважды; и когда в конце будет отображен XML-документ, должен получиться пустой элемент Employees. Вот результат:
Как и ожидалось, в последовательности есть два элемента Employee, подлежащие удалению. Видно, что первый из них — Alex Erohin — был удален. Однако не похоже, чтобы второй элемент был удален, и когда отображается результирующий документ XML, второй элемент Employee по-прежнему в нем присутствует. Перечисление ведет себя некорректно: налицо "проблема Хэллоуина". Имейте в виду, что "проблема Хэллоуина" не всегда проявляется одинаково. Иногда перечисление может прекратиться раньше, чем должно; иногда оно генерирует исключения. Поведение варьируется в зависимости от того, что именно произошло.
Так каково же решение? Решение в данном случае заключается в кэшировании элементов и выполнении перечисления по кэшу вместо обычной техники перечисления, которая полагается на внутренние указатели, повреждаемые в процессе удаления или модификации элементов. В рассматриваемом примере последовательность элементов будет кэшироваться с использованием стандартных операций запросов, специально предназначенных для кэширования, чтобы предотвратить проблемы с отложенным выполнением запроса. Будет применяться операция ТоArray.
Ниже показан тот же код, что и раньше, за исключением того, что вызывается операция ToArray и производится перечисление ее результата:
...
foreach (XElement x in elements.ToArray())
{
Console.WriteLine("Удаление: {0} = {1}", x.Name, x.Value);
x.Remove();
}
...
Этот код идентичен предыдущему, за исключением вызова операции ToArray в финальном перечислении, где удаляются элементы. Ниже показан результат:
Обратите внимание, что на этот раз выведены два сообщения, информирующих об удалении элемента Employee. Кроме того, когда отображается XML-документ после удаления, получается пустой элемент Employees, потому что все его дочерние элементы удалены. "Проблема Хэллоуина" решена!