LINQ to XML에는 XML 트리를 직접 수정할 수 있는 다양한 메서드가 포함되어 있습니다. 요소를 추가하고, 요소를 삭제하고, 요소의 내용을 변경하고, 특성을 추가하는 등의 작업을 수행할 수 있습니다. 이 프로그래밍 인터페이스는 XML 트리 수정에 설명되어 있습니다. 축 중 하나를 반복하는 경우(예: Elements축을 반복할 때 XML 트리를 수정하는 경우) 이상한 버그가 발생합니다.
이 문제를 "할로윈 문제"라고도 합니다.
컬렉션을 반복하는 LINQ를 사용하여 일부 코드를 작성하면 선언적 스타일로 코드를 작성하게 됩니다. 당신이 원하는 것을 설명하는 것이 어떻게 그것을 수행하고자 하는지를 설명하는 것보다 더 가깝습니다. 1) 첫 번째 요소를 가져오는 코드를 작성하는 경우 2) 일부 조건에 대해 테스트하고, 3) 수정하고, 4) 목록에 다시 넣으면 명령적 코드가 됩니다. 컴퓨터에서 원하는 작업을 수행하는 방법을 알려주고 있습니다.
동일한 작업에서 이러한 코드 스타일을 혼합하면 문제가 발생합니다. 다음을 살펴보세요.
세 개의 항목(a, b 및 c)이 포함된 연결된 목록이 있다고 가정합니다.
a -> b -> c
이제 세 개의 새 항목(a', b', c')을 추가하여 연결된 목록으로 이동하려는 경우를 가정해 보겠습니다. 결과 연결된 목록이 다음과 같이 표시되도록 합니다.
a -> a' -> b -> b' -> c -> c'
따라서 목록을 반복하는 코드를 작성하고 모든 항목에 대해 바로 그 뒤의 새 항목을 추가합니다. 코드는 먼저 a
요소를 확인하고 그 뒤에 a'
을 삽입합니다. 이제 코드가 목록의 다음 노드로 이동하므로 a a'
'와 b 사이에 새 항목이 목록에 추가됩니다.
이 문제를 어떻게 해결하시겠습니까? 글쎄, 원래 연결된 목록의 복사본을 만들고 완전히 새로운 목록을 만들 수 있습니다. 또는 순전히 명령적 코드를 작성하는 경우 첫 번째 항목을 찾아 새 항목을 추가한 다음 연결된 목록에서 두 번 진행하여 방금 추가한 요소를 넘어설 수 있습니다.
예: 반복하면서 추가하기
예를 들어 트리에 있는 모든 요소의 중복을 만드는 코드를 작성하려는 경우를 가정해 보겠습니다.
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
이 코드는 무한 루프로 이동합니다. 이 문장은 foreach
축을 반복하여 Elements()
요소에 새 요소를 추가합니다. 결국 방금 추가한 요소도 반복 처리하게 됩니다. 또한 루프를 반복할 때마다 새 개체를 할당하므로 결국 사용 가능한 모든 메모리를 사용합니다.
다음과 같이 표준 쿼리 연산자를 사용하여 ToList 컬렉션을 메모리로 끌어와 이 문제를 해결할 수 있습니다.
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)
이제 코드가 작동합니다. 결과 XML 트리는 다음과 같습니다.
<Root>
<A>1</A>
<B>2</B>
<C>3</C>
<A>1</A>
<B>2</B>
<C>3</C>
</Root>
예: 반복문을 사용하는 동안 삭제하기
특정 수준에서 모든 노드를 삭제하려는 경우 다음과 같은 코드를 작성하려고 할 수 있습니다.
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)
그러나, 이것은 당신이 원하는 것을하지 않습니다. 이 경우 첫 번째 요소 A를 제거한 후 루트에 포함된 XML 트리에서 제거되고 반복을 수행하는 Elements 메서드의 코드는 다음 요소를 찾을 수 없습니다.
이 예제는 다음과 같은 출력을 생성합니다.
<Root>
<B>2</B>
<C>3</C>
</Root>
해결 방법은 다음과 같이 컬렉션을 구체화하기 위해 호출 ToList 하는 것입니다.
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)
이 예제는 다음과 같은 출력을 생성합니다.
<Root />
또는 부모 요소를 호출 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)
예: LINQ에서 이러한 문제를 자동으로 처리할 수 없는 이유
한 가지 방법은 지연 평가를 수행하는 대신 항상 모든 데이터를 메모리로 가져오는 것입니다. 그러나 성능 및 메모리 사용 측면에서는 비용이 매우 많이 듭니다. 실제로 LINQ 및 LINQ to XML이 이 방법을 사용한다면 실제 상황에서는 실패합니다.
또 다른 가능한 방법은 일종의 트랜잭션 구문을 LINQ에 넣고 컴파일러가 코드를 분석하여 특정 컬렉션을 구체화해야 하는지 확인하는 것입니다. 그러나 부작용이 있는 모든 코드를 확인하는 것은 매우 복잡합니다. 다음 코드를 고려합니다.
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)
이러한 분석 코드는 TestSomeCondition 및 DoMyProjection 메서드와 해당 메서드가 호출한 모든 메서드를 분석하여 코드에 부작용이 있는지 확인해야 합니다. 그러나 분석 코드는 부작용이 있는 코드를 찾을 수 없습니다. 이 상황에서 자식 요소 root
에 부작용이 있는 코드만 선택해야 합니다.
LINQ to XML은 이러한 분석을 시도하지 않습니다. 이러한 문제를 피하는 것은 사용자의 달려 있습니다.
예: 선언적 코드를 사용하여 기존 트리를 수정하는 대신 새 XML 트리 생성
이러한 문제를 방지하려면 컬렉션의 의미 체계와 XML 트리를 수정하는 메서드의 의미 체계를 정확히 알고 있더라도 선언적 코드와 명령적 코드를 혼합하지 마세요. 문제를 방지하는 코드를 작성하는 경우 나중에 다른 개발자가 코드를 유지 관리해야 하며 문제가 명확하지 않을 수 있습니다. 선언적 코딩 스타일과 명령적 코딩 스타일을 혼합하면 코드가 더 취약해질 수 있습니다. 이러한 문제를 방지할 수 있도록 컬렉션을 구체화하는 코드를 작성하는 경우 유지 관리 프로그래머가 문제를 이해할 수 있도록 코드에 적절한 주석을 사용하여 기록해 둡니다.
성능 및 기타 고려 사항이 허용되는 경우 선언적 코드만 사용합니다. 기존 XML 트리를 수정하지 마세요. 대신 다음 예제와 같이 새 항목을 생성합니다.
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)
.NET