Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
LINQ to XML zawiera różne metody, które umożliwiają bezpośrednie modyfikowanie drzewa XML. Możesz dodawać elementy, usuwać elementy, zmieniać zawartość elementu, dodawać atrybuty itd. Ten interfejs programowania został opisany w temacie Modyfikowanie drzew XML. Jeśli iterujesz przez jedną z osi drzewa XML, takich jak Elements, i modyfikujesz drzewo XML podczas iteracji, możesz napotkać dziwne błędy.
Ten problem jest czasami znany jako "Problem Halloween".
Podczas pisania kodu przy użyciu linQ, który iteruje za pośrednictwem kolekcji, piszesz kod w stylu deklaratywnym. Jest to bardziej opis jak co chcesz, a nie sposób, w jaki chcesz to zrobić. Jeśli napiszesz kod, który 1) pobiera pierwszy element, 2) testuje go pod kątem pewnego warunku, 3) modyfikuje go, a 4) umieszcza go z powrotem na liście, będzie to kod imperatywny. Mówisz komputerowi , jak zrobić to, co chcesz zrobić.
Mieszanie tych stylów kodu w tej samej operacji prowadzi do problemów. Rozważ następujące kwestie:
Załóżmy, że masz połączoną listę z trzema elementami (a, b i c):
a -> b -> c
Teraz załóżmy, że chcesz przejść przez połączoną listę, dodając trzy nowe elementy (a', b'i c'). Chcesz, aby wynikowa połączona lista wyglądała następująco:
a -> a' -> b -> b' -> c -> c'
Dlatego napiszesz kod, który iteruje za pośrednictwem listy, a dla każdego elementu dodaje nowy element bezpośrednio po nim. Dzieje się tak, że kod najpierw zobaczy a
element i wstawi a'
go po nim. Teraz twój kod przejdzie do następnego węzła na liście, którym jest a'
, więc doda nowy element między a i b do listy.
Jak rozwiązać ten problem? Możesz utworzyć kopię oryginalnej połączonej listy i utworzyć zupełnie nową listę. Lub jeśli piszesz wyłącznie kod imperatywny, możesz znaleźć pierwszy element, dodać nowy element, a następnie przejść dwa kroki dalej w liście połączonej, przechodząc nad właśnie dodanym elementem.
Przykład: dodawanie podczas iteracji
Załóżmy na przykład, że chcesz napisać kod, aby utworzyć duplikat każdego elementu w drzewie:
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
Ten kod przechodzi w nieskończoną pętlę. Instrukcja foreach
przebiega przez oś Elements()
, dodając nowe elementy do elementu doc
. Kończy się iterowanie również przez elementy, które właśnie dodał. A ponieważ przydziela nowe obiekty z każdą iterację pętli, ostatecznie będzie zużywać całą dostępną pamięć.
Ten problem można rozwiązać, ściągając kolekcję do pamięci przy użyciu standardowego ToList operatora zapytania w następujący sposób:
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)
Teraz kod działa. Wynikowe drzewo XML jest następujące:
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
Przykład: usuwanie podczas iteracji
Jeśli chcesz usunąć wszystkie węzły na określonym poziomie, możesz być kuszony, aby napisać kod podobny do następującego:
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)
Nie robi to jednak tego, co chcesz. W takiej sytuacji po usunięciu pierwszego elementu element A zostanie usunięty z drzewa XML zawartego w katalogu głównym, a kod w metodzie Elements, który wykonuje iterację, nie może odnaleźć następnego elementu.
Ten przykład generuje następujące wyniki:
<Root>
<B>2</B>
<C>3</C>
</Root>
Rozwiązaniem ponownie jest wywołanie ToList w celu zmaterializowania kolekcji w następujący sposób:
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)
Ten przykład generuje następujące wyniki:
<Root />
Alternatywnie, można całkowicie wyeliminować iterację, wywołując RemoveAll na elemencie nadrzędnym.
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)
Przykład: dlaczego LINQ nie może automatycznie obsłużyć tych problemów
Jednym z podejść byłoby zawsze wprowadzenie wszystkiego do pamięci zamiast robić leniwą ocenę. Jednak byłoby to bardzo kosztowne pod względem wydajności i użycia pamięci. W rzeczywistości, jeśli LINQ i LINQ to XML miałyby przyjąć to podejście, nie sprawdzi się w rzeczywistych sytuacjach.
Innym możliwym podejściem jest umieszczenie jakiejś składni transakcji w LINQ i próba przeanalizowania kodu przez kompilator w celu określenia, czy dana kolekcja musi zostać zmaterializowana. Jednak próba określenia całego kodu, który ma skutki uboczne, jest niezwykle złożona. Rozważ następujący kod:
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)
Taki kod analizy musiałby przeanalizować metody TestSomeCondition i DoMyProjection oraz wszystkie metody wywoływane przez te metody, aby ustalić, czy jakikolwiek kod miał skutki uboczne. Jednak kod analizy nie mógł po prostu wyszukać żadnego kodu, który miał skutki uboczne. Trzeba w tej sytuacji wybrać tylko ten kod, który miał skutki uboczne dla elementów podrzędnych root
.
LINQ to XML nie próbuje wykonać żadnej takiej analizy. Należy unikać tych problemów.
Przykład: użyj kodu deklaratywnego, aby wygenerować nowe drzewo XML, a nie zmodyfikować istniejącego drzewa
Aby uniknąć takich problemów, nie mieszaj deklaratywnego i imperatywnego kodu, nawet jeśli znasz dokładnie semantyka kolekcji i semantyka metod modyfikujących drzewo XML. Jeśli napiszesz kod, który unika problemów, w przyszłości będzie musiany być utrzymywany przez innych deweloperów, którzy mogą nie mieć tak jasnego pojęcia o tych zagadnieniach. Jeśli mieszasz style kodowania deklaratywnego i imperatywnego, twój kod będzie bardziej kruchy. Jeśli napiszesz kod, który zmaterializuje kolekcję, aby uniknąć tych problemów, zanotuj to w komentarzach w kodzie, aby programiści zajmujący się konserwacją zrozumieli problem.
Jeśli wydajność i inne względy na to pozwalają, używaj tylko kodu deklaratywnego. Nie modyfikuj istniejącego drzewa XML. Zamiast tego wygeneruj nowy, jak pokazano w poniższym przykładzie:
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)