Erros de código declarativos/imperativos mistos (LINQ to XML)

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

Esse problema é às vezes conhecido como “o problema do Dia De Bruxas”.

Ao escrever algum código usando LINQ que itera por meio 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 fazer. Se você escreve o código que 1) obtém o primeiro elemento, 2) testá-la para alguma condição, 3) altera-a, e 4) coloque-a de novo na lista, então este código seria obrigatório. Você está informando o computador como fazer o que você quer que seja feito.

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

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

a -> b -> c

Agora, suponha que você deseja mover através da lista vinculada, adicionando novos itens três (a, b, e c#). Você deseja a lista vinculada resultante para ter esta aparência:

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

Assim você escreve o código que itera através da lista, e para cada item, adicione um novo item mesmo após ele. O que acontece são que seu código verá o primeiro elemento de a , e inserção a' após ele. Agora, seu código irá para o próximo nó na 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 do original associado para listar, e criar uma lista completamente nova. Ou se estiver escrevendo código puramente imperativo, você pode encontrar o primeiro item, adicionar o novo item e avançar duas vezes na lista vinculada, avançando sobre o elemento que acabou de adicionar.

Exemplo: adicionar enquanto itera

Por exemplo, suponha que você queira escrever um 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

Esse código entra em um loop infinito. A declaração de foreach itera através do eixo de Elements() , adicionando novos elementos para o elemento de doc . Acaba também iterar através dos elementos que acabou de adicionar. E como atribuir novos objetos com cada iteração do loop, consumirá se houver qualquer memória disponível.

Você pode corrigir este problema recebendo a coleção na memória usando o operador padrão de consulta de ToList , como 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())
    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. 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 enquanto itera

Se você deseja excluir todos os nós em um determinado nível, você pode ter tentado 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 dos elementos que está fazendo a iteração não consegue localizar o próximo elemento.

Esse exemplo gera a saída a seguir:

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

A solução é novamente chamar ToList para materializar a coleção, como 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)

Esse exemplo gera a saída a seguir:

<Root />

Como alternativa, você pode eliminar a iteração completamente chamando RemoveAll no 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 consegue lidar com esses problemas automaticamente

Uma abordagem seria sempre trazer tudo na memória em vez de fazer a avaliação lazy. No entanto, seria muito cara em termos de uso de desempenho e de memória. De fato, se LINQ e LINQ to XML usasse essa abordagem, 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 precisa ser materializada. No entanto, tentar determinar qualquer código que tiver efeitos colaterais é incredibly complexa. 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 métodos TestSomeCondition e DoMyProjection, e todos os métodos que esses métodos chamados a partir, para determinar se qualquer código tinha efeitos colaterais. Mas o código de análise não pode apenas procurar qualquer código que tem efeitos colaterais. Precisaria para selecionar apenas o código que tinha efeitos colaterais em elementos filho de root nesta situação.

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

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

Para evitar esses problemas, não misture códigos declarativos e imperativos, mesmo que você saiba exatamente a semântica de suas coleções e a semântica dos métodos que modificam a árvore XML. Se você escrever um código que evita problemas, seu código precisará ser mantido por outros desenvolvedores no futuro e eles podem não conhecer os problemas tão bem. Se você mistura estilos declarativo e obrigatórias de codificação, seu código será mais frágil. Se você escreve o código que materializa uma coleção para que esses problemas são impedidos, observar-la com comentários apropriadas em seu código, para que os desenvolvedores de aplicativos compreendam o problema.

Se o desempenho e outras considerações permitirem, use apenas código declarativo. Não altere 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)