Condividi tramite


Bug nel codice dichiarativo/imperativo misto (LINQ to XML)

LINQ to XML contiene i vari metodi che consentono di modificare direttamente un albero XML. È possibile aggiungere elementi, eliminare elementi, modificare il contenuto di un elemento, aggiungere attributi e così via. Questa interfaccia di programmazione è descritta in Modificare alberi XML. Se si esegue l'iterazione di uno degli assi, ad esempio Elements, e si modifica l'albero XML durante l'iterazione dell'asse, è possibile che vengano individuati alcuni bug strani.

Questo problema viene talvolta definito come "problema di Halloween".

Quando si scrive codice usando LINQ che esegue l'iterazione in una raccolta, si usa uno stile dichiarativo. Si tratta in pratica più di descrivere cosa si vuole eseguire, anziché come si vuole che venga eseguito. 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. In pratica si indica al computer come eseguire le operazioni desiderate.

È proprio la combinazione di questi stili di codice nella stessa operazione a comportare problemi. Considerare quanto segue:

Si supponga di disporre di un elenco collegato contenente tre elementi (a, b e c):

a -> b -> c

Si supponga ora di volersi spostare nell'elenco collegato, aggiungendo tre nuovi elementi (a', b' e c'). Si desidera che l'elenco collegato risultante sia simile al seguente:

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. Il codice rileverà quindi per primo l'elemento a e inserirà a' dopo di esso. A questo punto il codice passerà al nodo successivo nell'elenco, che è ora a', in modo da aggiungere un nuovo elemento tra a ' e b all'elenco.

Come si risolve questo problema? sarebbe possibile fare una copia dell'elenco collegato originale e creare un elenco completamente nuovo. In alternativa, se si scrive codice esclusivamente imperativo, è possibile individuare il primo elemento, aggiungere il nuovo elemento e quindi avanzare di due posizioni nell'elenco collegato oppure ignorare l'elemento appena aggiunto.

Esempio: Aggiunta durante l'iterazione

Si supponga, ad esempio, di voler scrivere codice per creare un duplicato di ogni elemento in un albero:

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. L'istruzione foreach scorre l'asse Elements(), aggiungendo nuovi elementi all'elemento doc, ma finisce per scorrere anche gli elementi appena aggiunti. Poiché inoltre alloca oggetti nuovi a ogni iterazione del ciclo, finisce con il consumare tutta la memoria disponibile.

Per risolvere questo problema, è possibile effettuare il pull della raccolta in memoria usando l'operatore di query standard ToList, come descritto di seguito:

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. l'albero XML risultante è la seguente:

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

Esempio: Eliminazione durante l'iterazione

Se si desidera eliminare tutti i nodi a un determinato livello, si può essere tentati di scrivere codice simile al seguente:

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, questo codice non consente di eseguire l'operazione desiderata. In questa situazione, dopo aver rimosso il primo elemento, A, l'elemento viene rimosso dall'albero XML contenuto nella radice e il codice del metodo Elements che esegue l'iterazione non riesce a individuare l'elemento successivo.

Nell'esempio viene prodotto l'output seguente:

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

Anche in questo caso la soluzione consiste nel chiamare ToList per materializzare la raccolta, come segue:

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:

<Root />

In alternativa, è possibile eliminare del tutto l'iterazione chiamando RemoveAll sull'elemento padre:

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 problemi

Un approccio potrebbe essere inserire sempre tutto in memoria anziché ricorrere alla valutazione lazy. Tuttavia, un tale approccio sarebbe molto oneroso in termini di prestazioni e utilizzo della memoria. Di fatto anche se questo approccio venisse usato con LINQ e LINQ to XML, non consentirebbe di ottenere risultati validi in situazioni realistiche.

Un altro possibile approccio potrebbe essere inserire in LINQ una qualche forma di sintassi delle transazione e fare in modo che il compilatore analizzi il codice per determinare se è necessario materializzare un'eventuale raccolta specifica. Tuttavia, il tentativo di determinare tutto il codice con effetti collaterali è incredibilmente complesso. Osservare il codice seguente:

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. Tuttavia il codice di analisi potrebbe non cercare solo il codice con effetti collaterali e dovrebbe selezionare solo quello che ha effetti collaterali sugli elementi figlio di root in questa situazione.

LINQ to XML non prova a eseguire un tale tipo di analisi. È responsabilità dell'utente evitare questi problemi.

Esempio: Usare il codice dichiarativo per generare un nuovo albero XML anziché modificare l'albero esistente

Per evitare questi 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. Se si scrive codice che evita problemi, il codice dovrà essere gestito da altri sviluppatori in futuro e potrebbe non fornire informazioni chiare sui problemi. Se si combinano stili di codifica dichiarativa e imperativa, il codice sarà più fragile. 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.

Se consentito dalle prestazioni e da altre considerazioni, usare solo codice dichiarativo. Non modificare l'albero XML esistente, Generarne invece un nuovo, come illustrato nell'esempio seguente:

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)