Errores en códigos declarativos/imperativos mixtos (C#) (LINQ to XML)
Actualización: November 2007
LINQ to XML incluye numerosos métodos que le permiten modificar directamente un árbol XML. Puede agregar elementos, eliminarlos, cambiar sus contenidos, agregar atributos, etc. Esta interfaz de programación se describe en Modificar árboles XML. Si está llevando a cabo una iteración por uno de los ejes, como puede ser Elements, y está modificando el árbol XML a medida que recorre el eje, es posible que acabe encontrando errores extraños.
En ocasiones, a este problema se le conoce como "El problema de Halloween".
Definición del problema
Cuando se escribe cierto código utilizando LINQ para iterar por una colección, se utiliza un código de estilo declarativo. Es un método que se aproxima más a definir qué es lo que desea, en vez de especificar cómo desea hacerlo. Si escribe un código que 1) obtenga el primer elemento, 2) lo compruebe con una cierta condición, 3) lo modifique y 4) lo coloque nuevamente en la lista, entonces se trata de un código imperativo. Le está indicando al ordenador cómo hacer aquello que desea.
Si se combinan ambos estilos de código en una misma operación, es cuando aparecen los problemas. Considere el siguiente caso:
Suponga que tiene una lista vinculada que contiene tres elementos (a, b y c):
a -> b -> c
Ahora, suponga que desea recorrer la lista vinculada y añadir tres nuevos elementos (a', b' y c'). Desea que la lista vinculada resultante tenga el siguiente aspecto:
a -> a' -> b -> b' -> c -> c'
De forma que escribe un código que recorre la lista y, que para cada elemento, añade uno nuevo justo después. Lo que ocurrirá es que el código encontrará primero el elemento a e insertará a' justo después. Ahora, el código pasará al siguiente nodo de la lista, que en este momento será a' Entonces agregará un nuevo elemento a la lista, a''.
¿Cómo se puede resolver esto en un caso real? Una opción es realizar una copia de la lista vinculada original y crear una lista completamente nueva. O bien, si únicamente está escribiendo un código imperativo, puede encontrar el primer elemento, añadir el nuevo y, a continuación, avanzar dos elementos en la lista, pasando así por encima del elemento recién agregado.
Agregar a medida que se va iterando
Suponga, por ejemplo, que desea escribir un cierto código que, para cada elemento de un árbol, cree un elemento duplicado:
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 entrará en un bucle infinito. La instrucción foreach lleva a cabo una iteración a lo largo del eje Elements(), agregando nuevos elementos al elemento doc. Al final, acaba realizando dicha iteración por los elementos que se acaban de agregar. Y, dado que coloca los nuevos objetos en cada una de las iteraciones del bucle, llegará un momento en el que consuma toda la memoria disponible.
Puede resolver este problema almacenando en memoria la colección mediante el operador de consulta estándar ToList<TSource>, de la siguiente 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())
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)
Ahora el código funciona correctamente. El árbol XML resultante es como sigue:
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
Eliminar a medida que se va iterando
Si desea eliminar todos los nodos que se encuentren en un cierto nivel, podría tener la tentación de escribir un código como el que sigue:
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)
Sin embargo, no realiza la tarea que deseaba. En esta situación, tras eliminar el primer elemento (A), éste se elimina del árbol XML contenido en la raíz, con lo que el código del método Elements encargado de la iteración no podrá encontrar el siguiente elemento.
Este código anterior genera el siguiente resultado:
<Root>
<B>2</B>
<C>3</C>
</Root>
De nuevo, la solución pasa por llamar a ToList<TSource> para materializar la colección, de la siguiente 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)
Esto genera el siguiente resultado:
<Root />
Como alternativa, puede eliminar la iteración entera llamando a RemoveAll en el elemento primario:
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)
¿Por qué LINQ no puede controlar este comportamiento?
Una posible aproximación sería trasladar todo a memoria en vez de realizar evaluaciones diferidas. No obstante, esto resultaría muy costoso en términos de rendimiento y de utilización de la memoria. De hecho, si LINQ y (LINQ to XML) utilizaran este método, no sería viable en situaciones reales.
Otra posible aproximación consiste en utilizar una cierta sintaxis transaccional en LINQ y hacer que el compilador intente analizar el código para determinar si es necesario materializar alguna colección en particular. Sin embargo, puede resultar extremadamente complejo analizar todo el código que pueda tener efectos secundarios. Considere el siguiente 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)
Un código de análisis así necesitaría analizar los métodos TestSomeCondition y DoMyProjection, así como todos los métodos a los que llaman, con el fin de determinar si existe código con efectos secundarios. Pero no bastaría con que el código de análisis buscara código que tuviera efectos secundarios. Sólo debería seleccionar aquel código que tuviera efectos secundarios sobre los elementos secundarios de root en este caso en particular.
LINQ to XML no realiza ese tipo de análisis.
Es responsabilidad del programador evitar este tipo de problemas.
Orientación
Primeramente, no mezcle código imperativo con código declarativo.
Incluso en el caso de que conozca con precisión la semántica de las colecciones y de los métodos que modifican el árbol XML y escriba un código que evite este tipo de problemas, éste deberá ser mantenido por otros desarrolladores en un futuro, y es posible que éstos no estén al tanto de esos problemas. Si mezcla estilos de programación declarativa e imperativa, el código resultará más complicado.
Si escribe código que materialice una colección de forma que se eviten todos estos problemas, incluya en él comentarios de forma que los programadores encargados de su mantenimiento puedan comprender la problemática.
En segundo lugar, si el rendimiento y otras consideraciones lo permiten, utilice únicamente código declarativo. No modifique el árbol XML existente. Genere uno nuevo.
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)