Fehler durch Vermischung von deklarativem und imperativem Code (LINQ to XML) (C#)Mixed Declarative Code/Imperative Code Bugs (LINQ to XML) (C#)

LINQ to XML enthält verschiedene Methoden, mit denen Sie eine XML-Struktur direkt ändern können. contains various methods that allow you to modify an XML tree directly. Sie können Elemente hinzufügen, Elemente löschen, den Inhalt eines Elements ändern, Attribute hinzufügen usw.You can add elements, delete elements, change the contents of an element, add attributes, and so on. Diese Programmierschnittstelle wird in Ändern von XML-Strukturen (LINQ to XML) (C#) beschrieben.This programming interface is described in Modifying XML Trees (LINQ to XML) (C#). Wenn Sie eine Iteration durch eine der Achsen, z. B. Elements durchlaufen, und Sie dabei die XML-Struktur ändern, kann es zu einer Reihe eigenartiger Fehler kommen.If you are iterating through one of the axes, such as Elements, and you are modifying the XML tree as you iterate through the axis, you can end up with some strange bugs.

Dieses Problem wird manchmal als "Halloween-Problem" bezeichnet.This problem is sometimes known as "The Halloween Problem".

Definition des ProblemsDefinition of the Problem

Wenn Sie unter Verwendung von LINQ Code schreiben, der eine Auflistung durchläuft, schreiben Sie Code in einem deklarativen Stil.When you write some code using LINQ that iterates through a collection, you are writing code in a declarative style. Dabei beschreiben Sie eher, was Sie möchten, statt zu beschreiben, wie Sie es möchten.It is more akin to describing what you want, rather that how you want to get it done. Wenn Sie Code schreiben würden, der 1.) das erste Element abruft, 2.) das Element auf bestimmte Bedingungen testet, 3.) das Element modifiziert und 4.) das Element dann wieder in die Liste setzt, wäre dies imperativer Code.If you write code that 1) gets the first element, 2) tests it for some condition, 3) modifies it, and 4) puts it back into the list, then this would be imperative code. Sie würden dem Computer damit sagen, wie er die von Ihnen gestellte Aufgabe ausführen soll.You are telling the computer how to do what you want done.

Wenn nun diese Formen von Code in ein und derselben Operation miteinander vermischt werden, treten Probleme auf.Mixing these styles of code in the same operation is what leads to problems. Nehmen wir einmal die folgende Situation:Consider the following:

Stellen Sie sich vor, Sie haben eine verknüpfte Liste mit drei Elementen darin (a, b und c):Suppose you have a linked list with three items in it (a, b, and c):

a -> b -> c

Stellen Sie sich nun vor, Sie möchten die verknüpfte Liste durchgehen und drei neue Elemente (a', b' und c') hinzufügen.Now, suppose that you want to move through the linked list, adding three new items (a', b', and c'). Die resultierende verknüpfte Liste soll dann wie folgt aussehen:You want the resulting linked list to look like this:

a -> a' -> b -> b' -> c -> c'

Sie schreiben also Code, der die Liste durchläuft, und der sofort anschließend für jedes Element ein neues Element hinzufügt.So you write code that iterates through the list, and for every item, adds a new item right after it. Nun geschieht Folgendes: Ihr Code sieht zuerst das a-Element und fügt dahinter a' ein.What happens is that your code will first see the a element, and insert a' after it. Der Code geht dann zum nächsten Knoten in der Liste, dieser Knoten ist aber jetzt a'.Now, your code will move to the next node in the list, which is now a'! Das stört den Code nicht, und er fügt der Liste ein neues Element, also a'', hinzu.It happily adds a new item to the list, a''.

Wie würden Sie dieses Problem im richtigen Leben lösen?How would you solve this in the real world? Nun, Sie könnten eine Kopie der ursprünglichen verknüpften Liste anlegen und eine vollkommen neue Liste erstellen.Well, you might make a copy of the original linked list, and create a completely new list. Sie könnten aber auch rein imperativen Code schreiben. Dieser würde dann das erste Element finden, das neue Element hinzufügen und dann in der verknüpften Liste zwei Knoten weitergehen und damit das Element übergehen, das Sie gerade hinzugefügt haben.Or if you are writing purely imperative code, you might find the first item, add the new item, and then advance twice in the linked list, advancing over the element that you just added.

Hinzufügungen beim DurchlaufenAdding While Iterating

Angenommen, Sie möchten Code schreiben, der für jedes Element in einer Struktur ein Duplikat erstellt.For example, suppose you want to write some code that for every element in a tree, you want to create a duplicate element:

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));  

Dieser Code führt zu einer Endlosschleife.This code goes into an infinite loop. Die foreach-Anweisung durchläuft die Elements()-Achse und fügt dabei dem doc-Element neue Elemente hinzu.The foreach statement iterates through the Elements() axis, adding new elements to the doc element. Das Ergebnis ist, dass die Anweisung auch die gerade hinzugefügten Elemente durchläuftIt ends up iterating also through the elements it just added. und bei jedem Durchlaufen der Schleife neue Objekte zuweist. Irgendwann wird der gesamte verfügbare Arbeitsspeicher dafür in Beschlag genommen.And because it allocates new objects with every iteration of the loop, it will eventually consume all available memory.

Dieses Problem können Sie beheben, indem Sie die Auflistung mit dem ToList-Standardabfrageoperator in den Arbeitsspeicher ziehen. Dies ist im Folgenden dargestellt:You can fix this problem by pulling the collection into memory using the ToList standard query operator, as follows:

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);  

Jetzt funktioniert der Code.Now the code works. Die resultierende XML-Struktur sieht wie folgt aus:The resulting XML tree is the following:

<Root>  
  <A>1</A>  
  <B>2</B>  
  <C>3</C>  
  <A>1</A>  
  <B>2</B>  
  <C>3</C>  
</Root>  

Löschungen beim DurchlaufenDeleting While Iterating

Wenn Sie alle Knoten auf einer bestimmten Ebene löschen möchten, könnten Sie versucht sein, Code wie den folgenden zu schreiben:If you want to delete all nodes at a certain level, you might be tempted to write code like the following:

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);  

Ein solcher Code würde aber nicht zum gewünschten Ergebnis führen.However, this does not do what you want. Nachdem Sie das erste Element, A, entfernt haben, würde es aus der im Stamm enthaltenen XML-Struktur entfernt werden, sodass der Code in der Elements-Methode, der die Iteration vornimmt, das nächste Element nicht finden könnte.In this situation, after you have removed the first element, A, it is removed from the XML tree contained in root, and the code in the Elements method that is doing the iterating cannot find the next element.

Der oben genannte Code erzeugt die folgende Ausgabe:The preceding code produces the following output:

<Root>  
  <B>2</B>  
  <C>3</C>  
</Root>  

Auch hier besteht die Lösung darin, ToList aufzurufen, um die Auflistung wie folgt zu materialisieren:The solution again is to call ToList to materialize the collection, as follows:

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);  

Dies erzeugt die folgende Ausgabe:This produces the following output:

<Root />  

Alternativ dazu können Sie die Iteration ganz eliminieren, indem Sie für das übergeordnete Element RemoveAll aufrufen:Alternatively, you can eliminate the iteration altogether by calling RemoveAll on the parent element:

XElement root = new XElement("Root",  
    new XElement("A", "1"),  
    new XElement("B", "2"),  
    new XElement("C", "3")  
);  
root.RemoveAll();  
Console.WriteLine(root);  

Warum kann LINQ dieses Problem nicht automatisch lösen?Why Can't LINQ Automatically Handle This?

Eine Herangehensweise bestünde darin, immer alles in den Arbeitsspeicher zu holen, statt mit verzögerter Auswertung zu arbeiten.One approach would be to always bring everything into memory instead of doing lazy evaluation. Dies würde aber massive Leistungseinbußen und einen hohen Arbeitsspeicherbedarf mit sich bringen.However, it would be very expensive in terms of performance and memory use. Im praktischen Einsatz würden LINQ und LINQ to XML bei dieser Variante scheitern.In fact, if LINQ and (LINQ to XML) were to take this approach, it would fail in real-world situations.

Ein anderer möglicher Ansatz bestünde darin, eine gewisse Transaktionssyntax in LINQ einzubauen und den Compiler zu veranlassen, den Code zu analysieren und zu bestimmen, ob irgendeine Auflistung materialisiert werden muss.Another possible approach would be to put in some sort of transaction syntax into LINQ, and have the compiler attempt to analyze the code and determine if any particular collection needed to be materialized. Der Versuch, allen Code zu bestimmen, der Nebenwirkungen hat, ist aber eine unglaublich komplexe Angelegenheit.However, attempting to determine all code that has side-effects is incredibly complex. Betrachten Sie folgenden Code:Consider the following code:

var z =  
    from e in root.Elements()  
    where TestSomeCondition(e)  
    select DoMyProjection(e);  

Solch ein Analysecode müsste die Methoden TestSomeCondition und DoMyProjection sowie alle von diesen Methoden aufgerufenen Methoden analysieren, um zu bestimmen, ob Code mit Nebenwirkungen vorhanden ist.Such analysis code would need to analyze the methods TestSomeCondition and DoMyProjection, and all methods that those methods called, to determine if any code had side-effects. Der Analysecode kann aber nicht nur einfach nach Code mit Nebenwirkungen suchen.But the analysis code could not just look for any code that had side-effects. Er dürfte nur den Code auswählen, der in dieser Situation Nebenwirkungen auf die untergeordneten Elemente von root hat.It would need to select for just the code that had side-effects on the child elements of root in this situation.

LINQ to XML unternimmt keine Versuche, eine solche Analyse durchzuführen.LINQ to XML does not attempt to do any such analysis.

Für die Vermeidung dieser Probleme sind allein Sie zuständig.It is up to you to avoid these problems.

EmpfehlungenGuidance

Vermischen Sie deklarativen Code nicht mit imperativem Code.First, do not mix declarative and imperative code.

Auch wenn Sie die Semantik Ihrer Auflistungen und die Semantik der Methoden, die die XML-Struktur ändern, exakt kennen und "cleveren" Code schreiben, der diese Art von Problemen vermeidet, muss Ihr Code zukünftig von anderen Entwicklern gewartet werden, die sich vielleicht mit dieser Problematik nicht so gut auskennen.Even if you know exactly the semantics of your collections and the semantics of the methods that modify the XML tree, if you write some clever code that avoids these categories of problems, your code will need to be maintained by other developers in the future, and they may not be as clear on the issues. Wenn Sie deklarativen und imperativen Code mischen, ist der Code anfälliger.If you mix declarative and imperative coding styles, your code will be more brittle.

Wenn Sie Code schreiben, der eine Auflistung so materialisiert, dass diese Probleme vermieden werden, versehen Sie ihn mit entsprechenden Kommentaren, um die für die Wartung zuständigen Programmierer über die Problematik zu informieren.If you write code that materializes a collection so that these problems are avoided, note it with comments as appropriate in your code, so that maintenance programmers will understand the issue.

Wenn die Arbeitsgeschwindigkeit und andere Überlegungen es zulassen, sollten Sie immer nur deklarativen Code verwenden.Second, if performance and other considerations allow, use only declarative code. Ändern Sie nicht Ihre vorhandene XML-Struktur.Don't modify your existing XML tree. Generieren Sie stattdessen eine neue Struktur.Generate a new one.

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);  

Siehe auchSee Also

Advanced LINQ to XML Programming (C#) (Erweiterte LINQ to XML-Programmierung (C#))Advanced LINQ to XML Programming (C#)