Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
LINQ to XML contiene varios métodos que permiten modificar directamente un árbol XML. Puede agregar elementos, eliminar elementos, cambiar el contenido de un elemento, 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.
Este problema se conoce a veces como "El problema de Halloween".
Al escribir código mediante LINQ que recorre en iteración una colección, se escribe código en un estilo declarativo. Es más parecido a describir lo que quieres, en lugar de cómo quieres hacerlo. Si escribe código que 1) obtiene el primer elemento, 2) lo prueba para alguna condición, 3) lo modifica y 4) lo vuelve a colocar en la lista, entonces esto sería código imperativo. Estás diciendo al equipo cómo hacer lo que quieres hacer.
Mezclar estos estilos de código en la misma operación es lo que provoca problemas. Tenga en cuenta lo siguiente.
Suponga que tiene una lista vinculada que contiene tres elementos (a, b y c):
a -> b -> c
Supongamos que quieres recorrer la lista vinculada, agregando tres elementos nuevos (a', b', y c'). Quiere que la lista vinculada resultante tenga este aspecto:
a -> a' -> b -> b' -> c -> c'
Por lo tanto, escribe código que recorre en iteración la lista y, para cada elemento, agrega un nuevo elemento justo después. Lo que sucede es que el código verá primero el a
elemento e insertará a'
después de él. Ahora, tu código se moverá al siguiente nodo de la lista, que ahora es a'
, por lo que agrega un nuevo elemento entre 'a' y 'b' a la lista.
¿Cómo lo resolvería? Bueno, podrías hacer una copia de la lista vinculada original y crear una lista completamente nueva. O bien, si está escribiendo código puramente imperativo, es posible que encuentre el primer elemento, agregue el nuevo elemento y, a continuación, avance dos veces en la lista vinculada, avanzando por el elemento que acaba de agregar.
Ejemplo: Sumar durante la iteración
Por ejemplo, supongamos que desea escribir código para crear un duplicado de cada elemento de un árbol:
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 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 asigna nuevos objetos con cada iteración del bucle, finalmente consumirá toda la memoria disponible.
Para solucionar este problema, extraiga la colección en memoria mediante el ToList operador de consulta estándar, como se indica a continuación:
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. El árbol XML resultante es el siguiente:
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
Ejemplo: Eliminación durante la iteración
Si desea eliminar todos los nodos en un determinado nivel, es posible que tenga la tentación de escribir código como el siguiente:
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 se realiza la tarea deseada. En esta situación, después de quitar el primer elemento, A, se quita del árbol XML contenido en la raíz y el código del método Elements que realiza la iteración no encuentra el elemento siguiente.
En este ejemplo se genera la siguiente salida:
<Root>
<B>2</B>
<C>3</C>
</Root>
De nuevo, la solución consiste en llamar a ToList para materializar la colección de la siguiente manera:
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)
En este ejemplo se genera la siguiente salida:
<Root />
Como alternativa, puede eliminar la iteración por completo llamando en el elemento primario RemoveAll.
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)
Ejemplo: Por qué LINQ no puede controlar automáticamente estos problemas
Una posible aproximación sería trasladar todo a memoria en vez de realizar evaluaciones diferidas. Sin embargo, sería muy caro en términos de rendimiento y uso de memoria. De hecho, si LINQ y LINQ to XML adoptaran este enfoque, se produciría un error en situaciones reales.
Otro enfoque posible sería colocar algún tipo de sintaxis de transacción en LINQ y hacer que el compilador intente analizar el código para determinar si se necesita materializar alguna colección determinada. Sin embargo, intentar determinar todo el código que tiene efectos secundarios es increíblemente complejo. Observe el código siguiente:
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)
Dicho código de análisis tendría que analizar los métodos TestSomeCondition y DoMyProjection, y todos los métodos que esos métodos llamaran, para determinar si algún código tenía efectos colaterales. Sin embargo, el código de análisis no podía buscar simplemente cualquier código con efectos secundarios. Tendría que seleccionar solo el código que producía efectos secundarios en los elementos secundarios de root
en esta situación.
LINQ to XML no intenta realizar este análisis. Es necesario evitar estos problemas.
Ejemplo: Usar código declarativo para generar un nuevo árbol XML en lugar de modificar el árbol existente
Para evitar estos problemas, no combine código declarativo e imperativo, incluso si conoce exactamente la semántica de las colecciones y la semántica de los métodos que modifican el árbol XML. Si escribe código que evita problemas, otros desarrolladores deberán mantener el código en el futuro y es posible que no estén tan claros en los problemas. Si combina estilos de codificación declarativos e imperativos, el código será más frágil. Si escribe código que materializa una colección para evitar estos problemas, anote los comentarios según corresponda en el código para que los programadores de mantenimiento comprendan el problema.
Si el rendimiento y otras consideraciones permiten, use solo código declarativo. No modifique el árbol XML existente. En su lugar, genere uno nuevo como se muestra en el ejemplo siguiente:
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)