Freigeben über


Gemischte deklarative/imperative Codefehler (LINQ to XML)

LINQ to XML enthält verschiedene Methoden, mit denen Sie eine XML-Struktur direkt ändern können. Sie können Elemente hinzufügen, Elemente löschen, den Inhalt eines Elements ändern, Attribute hinzufügen usw. Diese Programmierschnittstelle wird in "XML-Strukturen ändern" beschrieben. Wenn Sie eine der Achsen, z. B. Elements, durchlaufen und dabei die XML-Struktur ändern, kann es zu einer Reihe eigenartiger Fehler kommen.

Dieses Problem wird manchmal als "The Halloween Problem" bezeichnet.

Wenn Sie Code mit LINQ schreiben, der eine Auflistung durchläuft, schreiben Sie Code in einem deklarativen Stil. Es ist mehr eine Beschreibung , was Sie wollen, als wie Sie es erledigt haben möchten. Wenn Sie Code schreiben, der 1) das erste Element abruft, überprüft 2) es auf einige Bedingung, 3) ändert sie, und 4) fügt es wieder in die Liste ein, dann wäre dies imperativer Code. Sie geben dem Computer an, wie Sie die gewünschten Aufgaben erledigen können.

Das Mischen dieser Codearten in demselben Vorgang führt zu Problemen. Beachte Folgendes:

Angenommen, Sie haben eine verknüpfte Liste mit drei Elementen darin (a, b und c):

a - b ->> c

Angenommen, Sie möchten durch die verknüpfte Liste navigieren und drei neue Elemente hinzufügen (a', b'und c'). Sie möchten, dass die resultierende verknüpfte Liste wie folgt aussieht:

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

Daher schreiben Sie Code, der die Liste durchläuft, und fügt für jedes Element direkt danach ein neues Element hinzu. Was passiert ist, dass Ihr Code zuerst das a Element sieht und danach a' einfügt. Jetzt wechselt Ihr Code zum nächsten Knoten in der Liste, der nun a' ist, also fügt es ein neues Element zwischen a' und b zur Liste hinzu!

Wie würden Sie dies lösen? Nun, Sie können eine Kopie der ursprünglichen verknüpften Liste erstellen und eine völlig neue Liste erstellen. Sie könnten aber auch rein imperativen Code schreiben. Dieser würde das erste Element finden und dann das neue Element hinzufügen. Danach würde der Code in der verknüpften Liste zweimal vorrücken und damit das Element überspringen, das Sie gerade hinzugefügt haben.

Beispiel: Hinzufügen beim Durchlaufen

Angenommen, Sie möchten Code schreiben, um ein Duplikat jedes Elements in einer Struktur zu erstellen:

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

Dieser Code gerät in eine Endlosschleife. Die foreach-Anweisung durchläuft die Elements()-Achse und fügt dem doc-Element neue Elemente hinzu. Das Ergebnis ist, dass die Anweisung auch die gerade hinzugefügten Elemente durchläuft Und da neue Objekte mit jeder Iteration der Schleife zugeordnet werden, verbraucht sie schließlich den gesamten verfügbaren Speicher.

Sie können dieses Problem beheben, indem Sie die Sammlung mithilfe des Standardmäßigen Abfrageoperators wie folgt in den ToList Arbeitsspeicher ziehen:

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)

Jetzt funktioniert der Code. Die resultierende XML-Struktur lautet wie folgt:

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

Beispiel: Löschen beim Durchlaufen

Wenn Sie alle Knoten auf einer bestimmten Ebene löschen möchten, sind Sie möglicherweise versucht, Code wie den folgenden zu schreiben:

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)

Dies macht jedoch nicht das, was Sie wollen. In dieser Situation passiert Folgendes: Nachdem Sie das erste Element (A) entfernt haben, wird es aus der XML-Struktur im Stamm entfernt, sodass der Code in der Elements-Methode, der die Iteration ausführt, das nächste Element nicht findet.

Dieses Beispiel erzeugt die folgende Ausgabe:

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

Die Lösung besteht darin, ToList aufzurufen, um die Sammlung wie folgt zu materialisieren:

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)

Dieses Beispiel erzeugt die folgende Ausgabe:

<Root />

Alternativ dazu können Sie die Iteration ganz eliminieren, indem Sie für das übergeordnete Element RemoveAll aufrufen:

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)

Beispiel: Warum LINQ diese Probleme nicht automatisch behandeln kann

Ein Ansatz wäre, alles immer ins Gedächtnis zu bringen, anstatt faule Auswertungen durchzuführen. Es wäre jedoch sehr teuer in Bezug auf Leistung und Speichernutzung. Wenn LINQ und LINQ to XML diesen Ansatz übernehmen würden, würde es in realen Situationen fehlschlagen.

Ein weiterer möglicher Ansatz wäre, eine Art Transaktionssyntax in LINQ einzufügen, damit der Compiler den Code analysieren kann, um festzustellen, ob eine bestimmte Sammlung materialisiert werden muss. Der Versuch, den gesamten Code mit Nebenwirkungen zu ermitteln, ist jedoch unglaublich komplex. Beachten Sie den folgenden Code:

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)

Dieser Analysecode müsste die Methoden TestSomeCondition und DoMyProjection und alle Methoden analysieren, die diese Methoden aufgerufen haben, um festzustellen, ob Code Nebenwirkungen hatte. Der Analysecode konnte jedoch nicht nur nach Code suchen, der Nebenwirkungen hatte. Er dürfte nur den Code auswählen, der in dieser Situation Nebenwirkungen auf die untergeordneten Elemente von root hat.

LINQ to XML versucht nicht, eine solche Analyse durchzuführen. Es liegt an Ihnen, diese Probleme zu vermeiden.

Beispiel: Verwenden sie deklarativen Code, um eine neue XML-Struktur zu generieren, anstatt die vorhandene Struktur zu ändern.

Um solche Probleme zu vermeiden, mischen Sie deklarativen und imperativen Code nicht, auch wenn Sie genau die Semantik Ihrer Auflistungen und die Semantik der Methoden kennen, die die XML-Struktur ändern. Wenn Sie Code schreiben, der Probleme vermeidet, muss Ihr Code in Zukunft von anderen Entwicklern gewartet werden, und sie verstehen die Probleme möglicherweise nicht so gut. Wenn Sie deklarative und imperative Codierungsstile kombinieren, ist Ihr Code eher spröde. Wenn Sie Code schreiben, der eine Sammlung materialisiert, sodass diese Probleme vermieden werden, versehen Sie ihn mit entsprechenden Kommentaren in Ihrem Code, damit Wartungsentwickler das Problem verstehen.

Wenn Leistung und andere Überlegungen zulassen, verwenden Sie nur deklarativen Code. Ändern Sie ihre vorhandene XML-Struktur nicht. Generieren Sie stattdessen ein neues, wie im folgenden Beispiel gezeigt:

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)