Mieszane błędy kodu deklaratywnego/imperatywnego (LINQ to XML)

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, takich jak Elements, i modyfikujesz drzewo XML podczas iterowania przez oś, możesz w końcu uzyskać kilka dziwnych usterek.

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 bardziej szczegółowe opisujące to, 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 źródła:

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 kod zostanie przeniesiony do następnego węzła na liście, czyli teraz a', więc doda nowy element między elementem a 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 imperatywne, możesz znaleźć pierwszy element, dodać nowy element, a następnie przejść dwa razy na liście połączonej, rozwijając się 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 iteruje przez Elements() oś, dodając nowe elementy do doc elementu. 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 jest wywołanie ToList metody 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 element nadrzędny:

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 powiedzie 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. Spójrzmy na poniższy 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. Należy wybrać tylko kod, który miał skutki uboczne dla elementów podrzędnych root w tej sytuacji.

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 pozwala uniknąć problemów, kod będzie musiał być utrzymywany przez innych deweloperów w przyszłości i może nie być tak jasny w przypadku problemów. 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 je z komentarzami odpowiednio w kodzie, aby programiści konserwatorzy zrozumieli problem.

Jeśli wydajność i inne zagadnienia są dozwolone, użyj 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)