今天在C#中使用SelectNodes的时候出现了一些怪现象,先从还原现场开始吧。

首先创建一个简单的XML文件来试验,还是就保存为test.xml

 
  
  1. <?xml version="1.0" encoding="utf-8" ?>   
  2. <root>   
  3.   <users job="salas">   
  4.     <user>   
  5.       <name>Joe</name>   
  6.       <age>17</age>   
  7.     </user>   
  8.     <user>   
  9.       <name>Kate</name>   
  10.       <age>12</age>   
  11.     </user>   
  12.     <user>   
  13.       <name>Parry</name>   
  14.       <age>66</age>   
  15.     </user>   
  16.     <user>   
  17.       <name>Qiqi</name>   
  18.       <age>32</age>   
  19.     </user>   
  20.   </users>   
  21.   <users job="developer">   
  22.     <user>   
  23.       <name>David</name>   
  24.       <age>23</age>   
  25.     </user>   
  26.     <user>   
  27.       <name>Eath</name>   
  28.       <age>54</age>   
  29.     </user>   
  30.   </users>   
  31. </root>  

 

下面是我的C#代码

 
  
  1. static void Main(string[] args)   
  2.         {   
  3.             XmlDocument doc = new XmlDocument();   
  4.             doc.Load("test.xml");  
  5.  
  6.             XmlElement root = doc.DocumentElement;   
  7.             XmlNode userCollection= root.SelectSingleNode("users[1]");   
  8.             XmlNodeList usersOfOne = userCollection.SelectNodes("user");  
  9.  
  10.  
  11.             XmlNode placeholder=doc.CreateElement("placeholder");   
  12.             channel.ReplaceChild(placeholder, usersOfOne.Item(0));  
  13.  
  14.             Console.WriteLine(usersOfOne.Count);  
  15.  
  16. }  
  17.  

代码逻辑很简单,就是想找到第一个<users>节点把它第一个<user>子节点替换一下。

关键在于替换之后,我原本想的是usersOfOne的个数应该保存着4个<user>节点,但是最终的结果只有1个,而且就只是那个被替换掉的那个节点。

 

继续试验,这次修改下C#代码,将替换的节点变成第二个<user>节点试试!?usersOfOne的个数就变成两个,包括第一和第二个节点。

研究SelectNodes源码(以下源代码都是在Reflector 6中查看的)

 
  
  1. public XmlNodeList SelectNodes(string xpath)   
  2.        {   
  3.            XPathNavigator navigator = this.CreateNavigator();   
  4.            if (navigator == null)   
  5.            {   
  6.                return null;   
  7.            }   
  8.            return new XPathNodeList(navigator.Select(xpath));   
  9.        }   

 

发现它返回一个XPathNodeList,再去看下它的构造函数

 
  
  1. public XPathNodeList(XPathNodeIterator nodeIterator)   
  2. {   
  3.     this.nodeIterator = nodeIterator;   
  4.     this.list = new List<XmlNode>();   
  5.     this.done = false;   

你会发现它创建了一个List<XmlNode>,但是并没有给它赋值。让我们再去看看Count这个属性

 
  
  1. public override int Count   
  2.        {   
  3.            get   
  4.            {   
  5.                if (!this.done)   
  6.                {   
  7.                    this.ReadUntil(0x7fffffff);   
  8.                }   
  9.                return this.list.Count;   
  10.            }   
  11.        }  

它返回的数就是构造函数里创建的List<XmlNode>的Count。再去看看Item()这个函数

 
  
  1. public override XmlNode Item(int index)   
  2.         {   
  3.             if (this.list.Count <= index)   
  4.             {   
  5.                 this.ReadUntil(index);   
  6.             }   
  7.             if ((index >= 0) && (this.list.Count &gt; index))   
  8.             {   
  9.                 return this.list[index];   
  10.             }   
  11.             return null;   
  12.         } 

同样的也是返回的List<XmlNode>中的值。

所以,我们可以解释上面实验代码中的怪现象了。

我们使用SelectNodes的时候,它并没有真正的将节点取出来,而是当我们调用了其它方法后(比如item()或者Count属性),才通过ReadUntil这个方法将它们的值保存到那个List<XmlNode>中。

Count这个属性能将0x7fffffff个节点保存下来(这也暗示我们最多能处理的节点个数!?),而Item这个函数只是把你需要的个数保存下来,(大家也可以去看看ReadUntil方法)后面因为我将现在的这个节点替换了,所以在Count的时候,它无法去迭代找到下个节点,所以在替换第二个节点的时候只保留下第一第二节点的原因。

我们修改下上面的代码如下:

 
  
  1. static void Main(string[] args)   
  2.         {   
  3.             XmlDocument doc = new XmlDocument();   
  4.             doc.Load("test.xml");  
  5.  
  6.             XmlElement root = doc.DocumentElement;   
  7.             XmlNode channel = root.SelectSingleNode("users[1]");   
  8.             XmlNodeList usersOfOne = channel.SelectNodes("user");  
  9.  
  10.             //在SelectNodes之后马上调用Count   
  11.             Console.WriteLine(usersOfOne.Count);  
  12.  
  13.  
  14.             XmlNode placeholder=doc.CreateElement("placeholder");   
  15.             channel.ReplaceChild(placeholder, usersOfOne.Item(0));  
  16.  
  17.             Console.WriteLine(usersOfOne.Count);  
  18.  
  19. }  

它就会像我预期的那样打印出结果了。尽管替换节点之后,输出的依然是4。

这个问题在调试的时候也比较难发现,因为你调试时查看usersOfOne.Count属性,相当于在源程序中执行了Count一样,所以在调试程序的时候,它会输出的结果也是4,导致程序在运行的时候和调试的时候表现不同。