Bugs de código declarativo/imperativo mistos (LINQ to XML)

LINQ to XML contém vários métodos que permitem modificar uma árvore XML diretamente. Você pode adicionar elementos, excluir elementos, alterar o conteúdo de um elemento, adicionar atributos e assim por diante. Esta interface de programação é descrita em Modificar árvores XML. Se você estiver iterando através de um dos eixos, como Elements, e estiver modificando a árvore XML à medida que itera pelo eixo, pode acabar com alguns bugs estranhos.

Este problema é por vezes conhecido como "O Problema do Dia das Bruxas".

Quando você escreve algum código usando LINQ que itera através de uma coleção, você está escrevendo código em um estilo declarativo. É mais parecido com descrever o que você quer, em vez de como você quer fazê-lo. Se você escrever um código que 1) obtém o primeiro elemento, 2) o testa para alguma condição, 3) o modifica e 4) o coloca de volta na lista, então isso seria um código imperativo. Você está dizendo ao computador como fazer o que você quer fazer.

Misturar esses estilos de código na mesma operação é o que leva a problemas. Considere o seguinte:

Suponha que você tenha uma lista vinculada com três itens (a, b e c):

a -> b -> c

Agora, suponha que você queira percorrer a lista vinculada, adicionando três novos itens (a', b' e c'). Você deseja que a lista vinculada resultante tenha esta aparência:

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

Assim, você escreve um código que itera pela lista e, para cada item, adiciona um novo item logo após ele. O que acontece é que seu código verá primeiro o a elemento e inserirá a' depois dele. Agora, seu código será movido para o próximo nó da lista, que é agora a', então ele adiciona um novo item entre a' e b à lista!

Como você resolveria isso? Bem, você pode fazer uma cópia da lista vinculada original e criar uma lista completamente nova. Ou, se você estiver escrevendo um código puramente imperativo, poderá encontrar o primeiro item, adicionar o novo item e avançar duas vezes na lista vinculada, avançando sobre o elemento que você acabou de adicionar.

Exemplo: Adicionar durante a iteração

Por exemplo, suponha que você queira escrever código para criar uma duplicata de cada elemento em uma árvore:

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

Este código entra em um loop infinito. A instrução foreach percorre o eixo Elements(), adicionando novos elementos ao elemento doc. Ele acaba iterando também através dos elementos que acabou de adicionar. E como ele aloca novos objetos a cada iteração do loop, ele acabará consumindo toda a memória disponível.

Você pode corrigir esse problema puxando a coleção para a memória usando o ToList operador de consulta padrão, da seguinte maneira:

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)

Agora o código funciona. A árvore XML resultante é a seguinte:

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

Exemplo: Excluir durante a iteração

Se você quiser excluir todos os nós em um determinado nível, você pode ser tentado a escrever código como o seguinte:

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)

No entanto, isso não faz o que você quer. Nessa situação, depois de remover o primeiro elemento, A, ele é removido da árvore XML contida na raiz e o código no método Elements que faz a iteração não pode encontrar o próximo elemento.

Este exemplo produz a seguinte saída:

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

A solução novamente é chamar ToList para materializar a coleção, da seguinte forma:

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)

Este exemplo produz a seguinte saída:

<Root />

Como alternativa, você pode eliminar completamente a iteração chamando RemoveAll o elemento pai:

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)

Exemplo: Por que o LINQ não pode lidar automaticamente com esses problemas

Uma abordagem seria sempre trazer tudo para a memória em vez de fazer avaliações preguiçosas. No entanto, seria muito caro em termos de desempenho e uso de memória. Na verdade, se o LINQ, e o LINQ to XML, adotassem essa abordagem, ela falharia em situações do mundo real.

Outra abordagem possível seria colocar algum tipo de sintaxe de transação no LINQ, e fazer com que o compilador tentasse analisar o código para determinar se alguma coleção específica precisava ser materializada. No entanto, tentar determinar todo o código que tem efeitos colaterais é incrivelmente complexo. Considere o seguinte código:

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)

Esse código de análise precisaria analisar os métodos TestSomeCondition e DoMyProjection, e todos os métodos que esses métodos chamavam, para determinar se algum código tinha efeitos colaterais. Mas o código de análise não podia simplesmente procurar qualquer código que tivesse efeitos colaterais. Seria necessário selecionar apenas o código que tinha efeitos colaterais nos elementos filho de root nesta situação.

O LINQ to XML não tenta fazer essa análise. Cabe a você evitar esses problemas.

Exemplo: Use código declarativo para gerar uma nova árvore XML em vez de modificar a árvore existente

Para evitar esses problemas, não misture código declarativo e imperativo, mesmo que você conheça exatamente a semântica de suas coleções e a semântica dos métodos que modificam a árvore XML. Se você escrever código que evite problemas, seu código precisará ser mantido por outros desenvolvedores no futuro, e eles podem não ser tão claros sobre os problemas. Se você misturar estilos de codificação declarativos e imperativos, seu código será mais frágil. Se você escrever um código que materialize uma coleção para que esses problemas sejam evitados, anote-o com comentários conforme apropriado em seu código, para que os programadores de manutenção entendam o problema.

Se o desempenho e outras considerações permitirem, use apenas código declarativo. Não modifique sua árvore XML existente. Em vez disso, gere um novo, conforme mostrado no exemplo a seguir:

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)