Bug del codice dichiarativo/imperativo misto (LINQ to XML)Mixed declarative/imperative code bugs (LINQ to XML)

LINQ to XML contiene diversi metodi che consentono di modificare direttamente un albero XML.LINQ to XML contains various methods that allow you to modify an XML tree directly. È possibile aggiungere elementi, eliminare elementi, modificare il contenuto di un elemento, aggiungere attributi e così via.You can add elements, delete elements, change the contents of an element, add attributes, and so on. Questa interfaccia di programmazione è descritta in modificare gli alberi XML.This programming interface is described in Modify XML trees. Se si scorre uno degli assi, ad esempio Elements , e si modifica l'albero XML durante l'iterazione nell'asse, è possibile che si siano rilevati bug strani.If you're iterating through one of the axes, such as Elements, and you're modifying the XML tree as you iterate through the axis, you can end up with some strange bugs.

Questo problema viene talvolta definito come "problema di Halloween".This problem is sometimes known as "The Halloween Problem".

Quando si scrive codice usando LINQ che esegue l'iterazione di una raccolta, si scrive il codice in uno stile dichiarativo.When you write some code using LINQ that iterates through a collection, you're writing code in a declarative style. Si tratta di una procedura più simile alla descrizione di ciò che si vuole.It's more akin to describing what you want, rather that how you want to get it done. Se invece si scrive codice che 1) ottiene il primo elemento, 2) lo verifica in base ad alcune condizioni, 3) lo modifica e 4) lo reinserisce nell'elenco, si usa lo stile del codice imperativo.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. Si sta dicendo al computer come eseguire le operazioni desiderate.You're telling the computer how to do what you want done.

È proprio la combinazione di questi stili di codice nella stessa operazione a comportare problemi.Mixing these styles of code in the same operation is what leads to problems. Considerare quanto segue:Consider the following:

Si supponga di disporre di un elenco collegato contenente tre elementi (a, b e c):Suppose you have a linked list with three items in it (a, b, and c):

a-> b-> ca -> b -> c

Si supponga ora di volersi spostare nell'elenco collegato, aggiungendo tre nuovi elementi (a', b' e c').Now, suppose that you want to move through the linked list, adding three new items (a', b', and c'). Si desidera che l'elenco collegato risultante sia simile al seguente:You want the resulting linked list to look like this:

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

Si scrive pertanto codice che scorre l'elenco e aggiunge un nuovo elemento subito dopo ogni elemento già esistente.So you write code that iterates through the list, and for every item, adds a new item right after it. Il codice rileverà quindi per primo l'elemento a e inserirà a' dopo di esso.What happens is that your code will first see the a element, and insert a' after it. A questo punto, il codice passerà al nodo successivo dell'elenco, che ora è a' , quindi aggiunge un nuovo elemento tra a e b all'elenco.Now, your code will move to the next node in the list, which is now a', so it adds a new item between a' and b to the list!

Come si risolve questo problema?How would you solve this? sarebbe possibile fare una copia dell'elenco collegato originale e creare un elenco completamente nuovo.Well, you might make a copy of the original linked list, and create a completely new list. In alternativa, se si sta scrivendo codice puramente imperativo, è possibile trovare il primo elemento, aggiungere il nuovo elemento e quindi avanzare due volte nell'elenco collegato, avanzando sull'elemento appena aggiunto.Or if you're 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.

Esempio: aggiunta durante l'iterazioneExample: Adding while iterating

Si supponga, ad esempio, di voler scrivere codice per creare un duplicato di ogni elemento di un albero:For example, suppose you want to write code to create a duplicate of every element in a tree:

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

Questo codice entra in un ciclo infinito.This code goes into an infinite loop. L'istruzione foreach scorre l'asse Elements(), aggiungendo nuovi elementi all'elemento doc,The foreach statement iterates through the Elements() axis, adding new elements to the doc element. ma finisce per scorrere anche gli elementi appena aggiunti.It ends up iterating also through the elements it just added. Poiché inoltre alloca oggetti nuovi a ogni iterazione del ciclo, finisce con il consumare tutta la memoria disponibile.And because it allocates new objects with every iteration of the loop, it will eventually consume all available memory.

Per risolvere questo problema, è possibile effettuare il pull della raccolta in memoria usando l'operatore di query standard ToList, come descritto di seguito: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);
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)

A questo punto il codice viene eseguito correttamente.Now the code works. l'albero XML risultante è la seguente: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>

Esempio: eliminazione durante l'iterazioneExample: Deleting while iterating

Se si desidera eliminare tutti i nodi a un determinato livello, si può essere tentati di scrivere codice simile al seguente: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);
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)

Tuttavia, questa operazione non esegue le operazioni desiderate.However, this doesn't do what you want. In questa situazione, dopo aver rimosso il primo elemento, A, questo viene rimosso dall'albero XML contenuto nella radice e il codice nel metodo Elements che esegue l'iterazione non riesce a trovare l'elemento successivo.In this situation, after you've removed the first element, A, it's removed from the XML tree contained in root, and the code in the Elements method that does the iterating can't find the next element.

Nell'esempio viene prodotto l'output seguente:This example produces the following output:

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

Anche in questo caso la soluzione consiste nel chiamare ToList per materializzare la raccolta, come segue: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);
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)

Nell'esempio viene prodotto l'output seguente:This example produces the following output:

<Root />

In alternativa, è possibile eliminare del tutto l'iterazione chiamando RemoveAll sull'elemento padre: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);
Dim root As XElement = _
    <Root>
        <A>1</A>
        <B>2</B>
        <C>3</C>
    </Root>
root.RemoveAll()
Console.WriteLine(root)

Esempio: perché LINQ non è in grado di gestire automaticamente questi problemiExample: Why LINQ can't automatically handle these issues

Un approccio potrebbe essere inserire sempre tutto in memoria anziché ricorrere alla valutazione lazy.One approach would be to always bring everything into memory instead of doing lazy evaluation. Tuttavia, un tale approccio sarebbe molto oneroso in termini di prestazioni e utilizzo della memoria.However, it would be very expensive in terms of performance and memory use. In realtà, se LINQ e LINQ to XML, dovevano adottare questo approccio, avrebbe avuto esito negativo in situazioni reali.In fact, if LINQ, and LINQ to XML, were to take this approach, it would fail in real-world situations.

Un altro possibile approccio potrebbe essere inserire una sorta di sintassi di transazione in LINQ e fare in modo che il compilatore provi ad analizzare il codice per determinare se è necessario materializzare una raccolta specifica.Another possible approach would be to put some sort of transaction syntax into LINQ, and have the compiler attempt to analyze the code to determine if any particular collection needed to be materialized. Tuttavia, il tentativo di determinare tutto il codice con effetti collaterali è incredibilmente complesso.However, attempting to determine all code that has side-effects is incredibly complex. Osservare il codice seguente:Consider the following 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)

Tale codice di analisi dovrebbe analizzare i metodi TestSomeCondition e DoMyProjection, nonché tutti i metodi da questi chiamati, per determinare se il codice presenta effetti collaterali.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. Tuttavia il codice di analisi potrebbe non cercare solo il codice con effetti collateraliBut the analysis code could not just look for any code that had side-effects. e dovrebbe selezionare solo quello che ha effetti collaterali sugli elementi figlio di root in questa situazione.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 non tenta di eseguire questa analisi.LINQ to XML doesn't attempt to do any such analysis. È possibile evitare questi problemi.It's up to you to avoid these problems.

Esempio: usare il codice dichiarativo per generare un nuovo albero XML anziché modificare l'albero esistenteExample: Use declarative code to generate a new XML tree rather than modify the existing tree

Per evitare tali problemi, non combinare codice dichiarativo e imperativo, anche se si conosce esattamente la semantica delle raccolte e la semantica dei metodi che modificano l'albero XML.To avoid such problems, don't mix declarative and imperative code, even if you know exactly the semantics of your collections and the semantics of the methods that modify the XML tree. Se si scrive codice per evitare problemi, è necessario che il codice venga gestito da altri sviluppatori in futuro e che non siano chiari sui problemi.If you write code that avoids problems, your code will need to be maintained by other developers in the future, and they may not be as clear on the issues. Se si combinano stili di codifica dichiarativa e imperativa, il codice sarà più fragile.If you mix declarative and imperative coding styles, your code will be more brittle. Se si scrive codice che materializza una raccolta allo scopo di evitare questi problemi, annotarlo in appositi commenti nel codice, per consentire ai programmatori che effettuano interventi di manutenzione di comprendere il problema.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.

Se le prestazioni e altre considerazioni consentono, usare solo codice dichiarativo.If performance and other considerations allow, use only declarative code. Non modificare l'albero XML esistente,Don't modify your existing XML tree. Generarne invece uno nuovo come illustrato nell'esempio seguente:Instead, generate a new one as shown in the following example:

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)