共用方式為


混合的宣告式/命令式程式碼 Bug (LINQ to XML)

LINQ to XML 包含各種方法,可讓您直接修改 XML 樹狀結構。 您可以加入項目、刪除項目、變更項目的內容、加入屬性等等。 修改 XML 樹狀結構會說明這個程式開發介面。 如果您要逐一查看其中一個座標軸 (例如 Elements),而且要在逐一查看座標軸時修改 XML 樹狀結構,您可以解決一些奇怪的 Bug。

這種問題有時候稱為「幽靈問題」。

當您使用可逐一查看集合的 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() 座標軸,並將新的項目加入到 doc 項目。 它也會透過剛才加入的項目結束反覆運算。 而且它會利用迴圈的每次反覆運算配置新物件,因此最終會消耗所有可用的記憶體。

您可以使用 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)