65.9K
CodeProject 正在变化。 阅读更多。
Home

在 foreach 循环中枚举 XML 数据

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (21投票s)

2004年10月6日

3分钟阅读

viewsIcon

100669

downloadIcon

892

本文展示了如何在 foreach 循环中枚举 XML 数据,就像数据在一个集合中一样。

引言

本文演示了如何使用 foreach 循环遍历 XML 文件中的数据,就像数据存储在一个对象集合中一样。 这里介绍的基本概念是 .NET 的 IEnumerable/IEnumerator 机制不一定必须与集合类一起使用。

背景

假设你有一个 XML 文件,其中包含一些你想向用户展示的数据。 有很多种方法可以将数据从 XML 文件获取到输出设备。 许多技术都涉及将整个文件读入某个数据结构,然后显示缓存数据的某个子集。 如果你可以直接“遍历 XML 文件”本身,并且只存储你想显示的值,那岂不是很好? 如果你可以使用一个简单的 foreach 循环来执行此迭代,那岂不是更好? 幸运的是,.NET Framework 允许通过 IEnumerableIEnumerator 接口实现这种类型的灵活性和优雅性。

在我们查看代码之前,这里有一个关于 foreach 循环如何操作的复习。 要迭代的对象必须实现 IEnumerable 接口。 IEnumerable 有一个方法 GetEnumerator(),它返回一个实现 IEnumerator 接口的对象。 IEnumerator 有三个公共成员

  • 一个名为 Current 的属性,它返回枚举值列表中的“当前”项。
  • 一个名为 MoveNext() 的方法,它将枚举器移动到值列表中的“下一个”项,如果没有更多的项可以迭代,则返回 false
  • 一个名为 Reset() 的方法,它将枚举器定位到值列表中的“第一个”项之前。

foreach 机制通过 Current 属性请求当前项之前,它将调用 MoveNext() 方法。 假设 MoveNext() 返回 true,则调用 Current,并且其返回值成为 foreach 循环范围内的“局部临时”变量。

使用代码

本文随附的示例项目非常简单。 它的目的是演示所提出的技术,但它可以很容易地扩展以适应更复杂的需求。 该示例包含一个包含客户信息的 XML 文件,具有一个非常简单的模式

CUSTOMERS.XML

<?xml version="1.0" encoding="utf-8" ?> 
<Customers>
 <Customer id="1">
 <FirstName>Abe</FirstName>
 <LastName>Lalice</LastName>
 <Orders>7</Orders>
 <Balance>340.95</Balance>
 </Customer>
 <Customer id="2">
 <FirstName>Mary</FirstName>
 <LastName>Poolsworth</LastName>
 <Orders>14</Orders>
 <Balance>3782.02</Balance>
 </Customer>
 <Customer id="3">
 <FirstName>Perth</FirstName>
 <LastName>Higgins</LastName>
 <Orders>1</Orders>
 <Balance>42.00</Balance>
 </Customer>
 <Customer id="4">
 <FirstName>David</FirstName>
 <LastName>Applegate</LastName>
 <Orders>2</Orders>
 <Balance>232.50</Balance>
 </Customer>
 <Customer id="5">
 <FirstName>Martha</FirstName>
 <LastName>Whithersby</LastName>
 <Orders>26</Orders>
 <Balance>19023.07</Balance>
 </Customer>
</Customers>

以下代码从 XML 文件中获取某些客户并将它们放入 ListBox 控件中。 这样做的好处是,从客户端(Customers 类的用户)的角度来看,无法知道数据是从 XML 文件中实时读取的。 这种实现方式被完全封装起来。

private void CustomerEnumerationForm_Load(object sender,
  System.EventArgs e)
{
 //
 // Here we create a Customers object and implicitly
 // use the CustomerEnumerator to iterate
 // over all of the customers in the XML file. 
 // From our perspective here, it seems
 // as though the Customers class has a collection
 // containing all of the customers.  In
 // reality, though, the CustomerEnumerator is reading
 // in each customer from Customers.xml 
 // one at a time, as they are requested by the foreach loop.
 //
 //
 // Note, Customers implements IDisposable.
 // This is necessary in case the foreach loop is
 // exited before iterating over all of the customers
 // in the file. You do not have to explicitly
 // invoke the Dispose method, however, because a finalizer
 // is in place to handle disposing of the
 // XMLTextReader if the client code does not.
 using( Customers customers = new Customers() )
 {
    foreach( Customer cust in customers )
        if( cust.Orders > 2 )
            this.lstCustomers.Items.Add( cust );
 }
}

Customers 类表示文件中的所有客户。

public class Customers : IEnumerable
{
 // When the foreach loop begins, this method is invoked 
 // so that the loop gets an enumerator to query.
 public IEnumerator GetEnumerator()
 {
  return new CustomerEnumerator();
 }
}

CustomerEnumerator 类包含从 XML 文件中读取客户数据的代码。

// Exposes the customer data found in Customers.xml.
public class CustomerEnumerator : IEnumerator
{
 // Data
 private readonly string fileName = @"..\..\Customers.xml";
 private XmlTextReader reader;


 // IEnumerator Members
 
 // Called when the enumerator needs to be reinitialized.
 public void Reset()
 {
  if( this.reader != null )
   this.reader.Close();
  System.Diagnostics.Debug.Assert( File.Exists( this.fileName ), 
   "Customer file does not exist!" );
  StreamReader stream = new StreamReader( this.fileName );
  this.reader         = new XmlTextReader( stream );
 }

 // Called just prior to Current being invoked.  
 // If true is returned then the foreach loop will
 // try to get another value from Current.  
 // If false is returned then the foreach loop terminates.
 public bool MoveNext()
 {
  // Call Reset the first time MoveNext is called 
  // instead of in the constructor 
  // so that we keep the stream open only as long as needed.
  if( this.reader == null )
   this.Reset();
   
  if( this.FindNextTextElement() )
   return true;
   
  // If there are no more text elements in the XML file then
  // we have read in all of the data 
  // and the foreach loop should end.
  this.reader.Close();
  return false;
 }

 // Invoked every time MoveNext() returns true. 
 // This extracts the values for the "current" customer from 
 // the XML file and returns that data packaged up as a Customer object.
 public object Current
 {
  get
  {
    // No need to call FindNextTextElement here
    // because it was called for us by MoveNext().
    string firstName = this.reader.Value;

    // Set 'val' to a default value in case the element was empty.
    string val = "";
    if( this.FindNextTextElement() )
    val = this.reader.Value;

    string lastName = val;

    // Set 'val' to a default value in case the element was empty.
    val = "0";
    if( this.FindNextTextElement() )
    val = this.reader.Value;

    int     orders;
    try   { orders = Int32.Parse( val ); }
    catch { orders = Int32.MinValue;     }

    // Set 'val' to a default value in case the element was empty.
    val = "0";
    if( this.FindNextTextElement() )
    val = this.reader.Value;

    decimal balance;
    try   { balance = Decimal.Parse( val ); }
    catch { balance = Decimal.MinValue;     }

    return new Customer( firstName, lastName, orders, balance );
  }
 }


 // Private Helper

 // Advances the XmlTextReader to the next Text
 // element in the XML stream.
 // Returns true if there is more data to be read
 // from the stream, else false.
 private bool FindNextTextElement()
 {
    bool readOn = this.reader.Read();
    bool prevTagWasElement = false;
    while( readOn && this.reader.NodeType != XmlNodeType.Text )
    {
        // If the current element is empty, stop reading and return false.
        if( prevTagWasElement &&  this.reader.NodeType == XmlNodeType.EndElement )
            readOn = false;
        prevTagWasElement = this.reader.NodeType == XmlNodeType.Element;
        readOn = readOn && this.reader.Read();
    }
    return readOn;
 }
}

有一个非常简单的 Customer 类,它只是保存从 XML 文件中提取的数据,以便枚举器可以捆绑有关单个客户的所有数据。 我不会在这里展示它,因为该类中没有任何与本文主题相关的内容。 不过,它在示例项目中可用。

结论

虽然这个新颖的想法可能不是软件开发的下一个银弹,但我希望你觉得它有用或至少有趣。 我知道我是这么认为的,否则我就不会费心写一篇关于它的文章了!:)

更新

  • 2005年1月9日 - 修复了 FindNextTextElement() 以处理元素不包含 TextElement 的情况。 还在 CustomersCustomerEnumerator 上实现了 IDisposable,并向 CustomerEnumerator 添加了终结器。 所有这些方法都是为 CustomerEnumerator 的用户在遍历文件中的所有元素之前从 foreach 循环中断开的情况添加的。 需要有一种方法来关闭 XmlTextReader
© . All rights reserved.