Sdílet prostřednictvím


Smíšené deklarativní nebo imperativní chyby kódu (LINQ to XML)

LINQ to XML obsahuje různé metody, které umožňují upravit strom XML přímo. Můžete přidat prvky, odstranit prvky, změnit obsah elementu, přidat atributy atd. Toto programovací rozhraní je popsáno v úpravě stromů XML. Pokud procházíte některou z os, například Elementsa upravujete strom XML při iteraci osou, můžete skončit s některými podivnými chybami.

Tento problém se někdy označuje jako "Halloween Problem".

Když napíšete kód pomocí LINQ, který prochází kolekcí, píšete kód v deklarativním stylu. Je to spíše podobné popisování toho, co chcete, a spíše to, jak to chcete udělat. Pokud napíšete kód, který 1) získá první prvek, 2) testuje ho pro určitou podmínku, 3) upraví ho a 4) vrátí do seznamu, pak by to byl imperativní kód. Říkáte počítači , jak udělat to, co chcete udělat.

Kombinování těchto stylů kódu ve stejné operaci je to, co vede k problémům. Zvažte použití těchto zdrojů:

Předpokládejme, že máte propojený seznam se třemi položkami (a, b a a c):

a -> b -> c

Předpokládejme, že chcete procházet propojený seznam a přidat tři nové položky (a', b' a c'). Chcete, aby výsledný propojený seznam vypadal takto:

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

Takže napíšete kód, který prochází seznamem a pro každou položku přidá novou položku hned za ní. Co se stane, je, že váš kód nejprve uvidí a prvek a vloží a' ho za něj. Teď se váš kód přesune do dalšího uzlu v seznamu, který je teď a', takže přidá novou položku mezi a b do seznamu.

Jak byste to vyřešili? Můžete vytvořit kopii původního propojeného seznamu a vytvořit úplně nový seznam. Nebo pokud píšete čistě imperativní kód, můžete najít první položku, přidat novou položku a pak přejít dvakrát do propojeného seznamu a přejít k prvku, který jste právě přidali.

Příklad: Přidání při iteraci

Předpokládejme například, že chcete napsat kód, který vytvoří duplikát každého prvku ve stromu:

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

Tento kód přejde do nekonečné smyčky. Příkaz foreach prochází osou Elements() a přidává do elementu doc nové prvky. Nakonec se iteruje také prostřednictvím prvků, které právě přidal. A protože přiděluje nové objekty s každou iterací smyčky, nakonec spotřebuje veškerou dostupnou paměť.

Tento problém můžete vyřešit načtením kolekce do paměti pomocí standardního operátoru ToList dotazu následujícím způsobem:

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)

Kód teď funguje. Výsledný strom XML je následující:

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

Příklad: Odstranění při iteraci

Pokud chcete odstranit všechny uzly na určité úrovni, může být lákavé psát kód takto:

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)

To ale nedělá to, co chcete. V této situaci se po odebrání prvního elementu A odebere ze stromu XML obsaženého v kořenovém adresáři a kód v metodě Elements, která iterace nedokáže najít další prvek.

Tento příklad vytvoří následující výstup:

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

Dalším řešením je volat ToList materializaci kolekce následujícím způsobem:

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)

Tento příklad vytvoří následující výstup:

<Root />

Alternativně můžete iteraci úplně eliminovat voláním RemoveAll nadřazeného prvku:

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)

Příklad: Proč LINQ nemůže tyto problémy automaticky zpracovat

Jedním z přístupů by bylo vždy přenést všechno do paměti místo toho, aby se provádělo opožděné hodnocení. Z hlediska výkonu a využití paměti by však bylo velmi nákladné. Ve skutečnosti, pokud by linQ a LINQ to XML, tento přístup by selžel v reálných situacích.

Dalším možným přístupem by bylo vložit do LINQ určitou syntaxi transakce a kompilátor se pokusí analyzovat kód, aby určil, jestli je potřeba nějaká konkrétní kolekce materializovat. Pokus o určení veškerého kódu, který má vedlejší účinky, je ale neuvěřitelně složitý. Uvažujte následující kód:

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)

Takový kód analýzy by musel analyzovat metody TestSomeCondition a DoMyProjection a všechny metody, které tyto metody volají, k určení, zda nějaký kód měl vedlejší účinky. Kód analýzy ale nemohl jen hledat žádný kód, který měl vedlejší účinky. Bylo by potřeba vybrat pouze kód, který měl vedlejší účinky na podřízené root prvky v této situaci.

LINQ to XML se nepokouší provést žádnou takovou analýzu. Je na vás, abyste se těmto problémům vyhnuli.

Příklad: Použití deklarativního kódu k vygenerování nového stromu XML místo úpravy existujícího stromu

Chcete-li se těmto problémům vyhnout, nekombinujte deklarativní a imperativní kód, i když znáte přesně sémantiku kolekcí a sémantiku metod, které upravují strom XML. Pokud napíšete kód, který se vyhne problémům, bude potřeba, aby váš kód v budoucnu udržovali jiní vývojáři a nemusí být tak jasné, co se týče problémů. Pokud kombinujete deklarativní a imperativní styly kódování, bude váš kód více křehký. Pokud napíšete kód, který materializuje kolekci, aby se těmto problémům vyhnuli, poznamenejte si ho s komentáři podle potřeby v kódu, aby programátoři údržby pochopili problém.

Pokud výkon a další aspekty umožňují, použijte pouze deklarativní kód. Neupravujte stávající strom XML. Místo toho vygenerujte nový, jak je znázorněno v následujícím příkladu:

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)