Объектная модель LINQ to XML

49

С новым API-интерфейсом LINQ to XML пришла новая объектная модель, содержащая множество новых классов, находящихся в пространстве имен System.Xml.Linq. Один из них — статический класс, в котором находятся расширяющие методы LINQ to XML; два класса компараторов — XNodeDocumentOrderComparer и XNodeEqualityComparer; остальные классы используются для построения деревьев XML. Эти остальные классы показаны на диаграмме, представленной на рисунке:

Объектная модель LINQ to XML

Следует отметить некоторые интересные вещи:

Все эти классы используются для построения деревьев 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, потому что все его дочерние элементы удалены. "Проблема Хэллоуина" решена!

Пройди тесты
Лучший чат для C# программистов