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)