2013-03-28 12 views
9

Si consideri il seguente codice:Linq, XmlNodes, foreach ed eccezioni

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Xml; 
using System.Xml.Linq; 

namespace ConsoleApplication1 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      XmlDocument xmlDoc = new XmlDocument(); 

      xmlDoc.LoadXml(@"<Parts> 
    <Part name=""DisappearsOk"" disabled=""true""></Part> 
    <Part name=""KeepMe"" disabled=""false""></Part> 
    <Part name=""KeepMe2"" ></Part> 
    <Part name=""ShouldBeGone"" disabled=""true""></Part> 
</Parts>"); 

      XmlNode root = xmlDoc.DocumentElement; 
      List<XmlNode> disabledNodes = new List<XmlNode>(); 

      try 
      { 

       foreach (XmlNode node in root.ChildNodes.Cast<XmlNode>() 
              .Where(child => child.Attributes["disabled"] != null && 
                  Convert.ToBoolean(child.Attributes["disabled"].Value))) 
       { 
        Console.WriteLine("Removing:"); 
        Console.WriteLine(XDocument.Parse(node.OuterXml).ToString()); 
        root.RemoveChild(node); 
       } 
      } 
      catch (Exception Ex) 
      { 
       Console.WriteLine("Exception, as expected"); 
      } 

      Console.WriteLine(); 
      Console.WriteLine(XDocument.Parse(root.OuterXml).ToString()); 

      Console.ReadKey(); 
     } 
    } 
} 

Quando eseguo questo codice in Visual Studio Express 2010 non ottengo un'eccezione, come previsto. Mi aspetterei uno perché sto rimuovendo qualcosa da una lista mentre lo itera.

Quello che faccio ottenere un elenco indietro con solo il primo nodo figlio rimosso:

enter image description here

Perché non ottengo un'eccezione operazione non valida?

Nota che il codice equivilent in IDEOne.com dà l'eccezione prevista: http://ideone.com/qoRBbb

Si noti inoltre che se tolgo tutto il LINQ (.Cast().Where()) ottengo lo stesso risultato, un solo nodo rimosso , nessuna eccezione.

C'è qualche problema con le mie impostazioni in VSExpress?


Nota che so esecuzione differita è coinvolto, ma mi aspetto la clausola where, quando iterato su di iterare sulla enumerazione sorgente (nota bambino), che darebbe l'eccezione mi aspetto.

Il mio problema è che non ottengo quell'eccezione in VSexpress, ma faccio in IDEOne (mi aspetterei che in entrambi/tutti i casi, o almeno in caso contrario, mi aspetterei il risultato corretto).


Da Wouter's answer sembra è invalidare l'iteratore, quando il primo figlio è stato rimosso, piuttosto che dare un'eccezione. C'è qualcosa di ufficiale che dice questo? Questo comportamento è previsto in altri casi? Chiamerei invalidare l'iteratore in silenzio piuttosto che con un'eccezione "Silenzioso ma mortale".

+0

Non vedo l'output di 'Console.WriteLine (" Rimozione: ");' anche nel risultato. Sei sicuro che il loop si spenga? – Impworks

+0

È in cima – IronMan84

+0

Siamo spiacenti, lo screenshot modificato (aggiunto quel controllo/dimostrazione dopo il test iniziale) –

risposta

2

Anche il seguente codice non buttare eccezioni:

foreach (XmlNode node in root.ChildNodes) 
    root.RemoveChild(node); 

E sarà rimuovere esattamente un elemento. Non sono al 100% che la mia spiegazione sia corretta, ma è sulla strada giusta. Quando si itera su una raccolta, si recupera il suo enumeratore. Per XmlNode, che è una raccolta, questa è una classe personalizzata denominata XmlChildEnumerator.

Se si cerca l'implementazione MoveNext tramite Reflector, si vedrà che l'enumeratore memorizza il nodo che sta attualmente esaminando. Quando chiami MoveNext, ti sposti sul fratello successivo.

Cosa succede nel codice sopra è che si ottiene il primo nodo dalla raccolta.L'enumeratore generato implicitamente nel corpo del ciclo foreach assume quel primo nodo come nodo corrente. Quindi, nel corpo del ciclo foreach rimuovi quel nodo.

Ora il nodo viene rimosso dall'elenco e l'esecuzione si sposta nuovamente su MoveNext. Tuttavia, dal momento che abbiamo appena rimosso il primo nodo dalla raccolta, è stato rimosso dalla raccolta e il nodo non ha alcun fratello. Poiché non esiste alcun fratello per il nodo, l'iterazione si interrompe e foreach chiude le uscite, rimuovendo così solo un singolo elemento.

Questo non genera eccezioni poiché non controlla se la raccolta è stata modificata, ma vuole solo passare al nodo successivo che può trovare. Ma poiché il nodo rimosso (scollegato) non appartiene a una raccolta, il ciclo si interrompe.

Spero che questo chiarisca il problema.

+0

Ottima spiegazione. Chiaramente le persone Mono devono avere l'enumeratore codificato in modo leggermente diverso, per causare l'eccezione (deliberatamente o meno). –

2

Poiché si sta iterando su ChildNodes, la rimozione del primo figlio invalida l'iteratore. Per questo motivo, l'iterazione si interromperà dopo la prima rimozione.

Se si divide il vostro filtraggio e iterazione, il codice sarà rimuovere tutti gli elementi:

var col = root.ChildNodes.Cast<XmlNode>() 
          .Where(child => child.Attributes["disabled"] != null && 
              Convert.ToBoolean(child.Attributes["disabled"].Value)).ToList(); 

foreach (XmlNode node in col) 
{ 
    Console.WriteLine("Removing:"); 
    Console.WriteLine(XDocument.Parse(node.OuterXml).ToString()); 
    root.RemoveChild(node); 
} 
+0

Sicuramente la clausola where causerà comunque l'iterazione della collezione sottostante? –

+2

Ma 'Where' dovrebbe essere valutato come pigro. Perché dovrebbe creare una raccolta separata e non eseguire lo streaming dei dati da 'root.ChildNodes'? – Impworks

+0

Provato quanto sopra in LINQPad 4, funziona – chridam