Gemengde declaratieve/imperatieve codefouten (LINQ naar XML)

LINQ naar XML bevat verschillende methoden waarmee u een XML-structuur rechtstreeks kunt wijzigen. U kunt elementen toevoegen, elementen verwijderen, de inhoud van een element wijzigen, kenmerken toevoegen, enzovoort. Deze programmeerinterface wordt beschreven in XML-structuren wijzigen. Als u een van de assen doorloopt, zoals Elements, en u de XML-structuur wijzigt terwijl u daarover doorloopt, kunt u op enkele vreemde bugs stuiten.

Dit probleem wordt ook wel 'Het Halloween-probleem' genoemd.

Wanneer u code schrijft met LINQ die door een verzameling wordt herhaald, schrijft u code in een declaratieve stijl. Het is meer verwant aan het beschrijven van wat u wilt, in plaats van hoe u het wilt doen. Als u code schrijft die 1) het eerste element krijgt, 2) test het voor een bepaalde voorwaarde, 3) wijzigt het en 4) zet het terug in de lijst, dan zou dit imperatieve code zijn. U vertelt de computer hoe u moet doen wat u wilt doen.

Het combineren van deze codestijlen in dezelfde bewerking leidt tot problemen. Houd rekening met het volgende:

Stel dat u een gekoppelde lijst hebt met drie items in de lijst (a, b en c):

a -> b -> c

Stel nu dat u door de gekoppelde lijst wilt gaan en drie nieuwe items (a', b' en c' wilt toevoegen). U wilt dat de resulterende gekoppelde lijst er als volgt uitziet:

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

U schrijft dus code die door de lijst wordt herhaald en voegt voor elk item een nieuw item toe direct erna. Wat er gebeurt, is dat uw code eerst het a element ziet en erna invoegt a' . Nu wordt de code verplaatst naar het volgende knooppunt in de lijst. Dit is nu a', dus er wordt een nieuw item tussen a' en b toegevoegd aan de lijst.

Hoe zou je dit oplossen? Misschien maakt u een kopie van de oorspronkelijke gekoppelde lijst en maakt u een volledig nieuwe lijst. Of als u puur imperatieve code schrijft, vindt u mogelijk het eerste item, voegt u het nieuwe item toe en gaat u vervolgens twee keer verder in de gekoppelde lijst en gaat u verder met het element dat u zojuist hebt toegevoegd.

Voorbeeld: Toevoegen tijdens het herhalen

Stel dat u code wilt schrijven om een duplicaat te maken van elk element in een boomstructuur:

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

Deze code gaat in een oneindige lus. De foreach instructie doorloopt de Elements() as en voegt nieuwe elementen toe aan het doc element. Het itereert uiteindelijk ook door de elementen die het zojuist heeft toegevoegd. En omdat er nieuwe objecten worden toegewezen met elke iteratie van de lus, verbruikt het uiteindelijk al het beschikbare geheugen.

U kunt dit probleem als volgt oplossen door de verzameling in het geheugen op te halen met behulp van de ToList standaardqueryoperator:

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)

De code werkt nu. De resulterende XML-structuur is het volgende:

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

Voorbeeld: Verwijderen tijdens het herhalen

Als u alle knooppunten op een bepaald niveau wilt verwijderen, bent u misschien geneigd om code als volgt te schrijven:

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)

Dit doet echter niet wat u wilt. In deze situatie, nadat u het eerste element, A, hebt verwijderd, wordt het uit de XML-boom in de wortel verwijderd, en de code in de Elements-methode die het itereren uitvoert, kan het volgende element niet vinden.

In dit voorbeeld wordt de volgende uitvoer gegenereerd:

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

De oplossing is opnieuw om ToList aan te roepen om de verzameling als volgt te materialiseren:

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)

In dit voorbeeld wordt de volgende uitvoer gegenereerd:

<Root />

U kunt de iteratie ook helemaal elimineren door RemoveAll aan te roepen op het bovenliggende element.

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)

Voorbeeld: Waarom LINQ deze problemen niet automatisch kan afhandelen

Een benadering is om altijd alles in het geheugen te brengen in plaats van luie evaluatie uit te voeren. Het zou echter erg duur zijn in termen van prestaties en geheugengebruik. Als LINQ en LINQ naar XML zouden worden gebruikt, zou deze benadering in werkelijkheid mislukken.

Een andere mogelijke methode is om een soort transactiesyntaxis in LINQ te plaatsen en de compiler de code te laten analyseren om te bepalen of een bepaalde verzameling moet worden gerealiseerd. Het is echter ongelooflijk complex om alle code te bepalen die bijwerkingen heeft. Houd rekening met de volgende 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)

Deze analysecode moet de methoden TestSomeCondition en DoMyProjection analyseren, en alle methoden die door deze methoden worden aangeroepen, om te bepalen of code neveneffecten had. Maar de analysecode kon niet alleen zoeken naar code die neveneffecten had. Het zou alleen de code moeten selecteren die bijwerkingen had op de kind elementen van root in deze situatie.

LINQ naar XML probeert geen dergelijke analyse uit te voeren. Het is aan u om deze problemen te voorkomen.

Voorbeeld: Declaratieve code gebruiken om een nieuwe XML-structuur te genereren in plaats van de bestaande structuur te wijzigen

Als u dergelijke problemen wilt voorkomen, moet u geen declaratieve en imperatieve code combineren, zelfs als u precies de semantiek van uw verzamelingen en de semantiek van de methoden kent die de XML-structuur wijzigen. Als u code schrijft die problemen voorkomt, moet uw code in de toekomst door andere ontwikkelaars worden onderhouden en zijn ze mogelijk niet zo duidelijk over de problemen. Als u declaratieve en imperatieve coderingsstijlen combineert, is uw code kwetsbaarder. Als u code schrijft waarmee een verzameling wordt gerealiseerd, zodat deze problemen worden vermeden, noteert u deze met opmerkingen in uw code, zodat onderhoudsprogrammeurs het probleem begrijpen.

Als prestaties en andere overwegingen dit toestaan, gebruikt u uitsluitend declaratieve code. Wijzig de bestaande XML-structuur niet. Genereer in plaats daarvan een nieuwe, zoals wordt weergegeven in het volgende voorbeeld:

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)