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

LINQ to XML

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (66投票s)

2008年3月14日

CPOL

8分钟阅读

viewsIcon

338049

downloadIcon

4892

对 .NET Framework 3.5 中 LINQ 和 XML 的探讨。

引言

使用 Microsoft 的 .NET Framework 2.0 及更早版本处理 XML 是一项繁琐的任务。可用的 API 遵循 W3C DOM 模型,并且是文档为中心的。一切都始于文档;没有文档就无法创建元素;即使是 XML 片段也是一个文档。

然而,在 .NET Framework 的最新版本中,这种情况已经改变。XML 现在是面向元素的。借助对象初始化和匿名类型等功能,创建 XML 非常容易。再加上 LINQ 的功能,我们现在拥有了一个非常易于使用且功能强大的 XML 工具。

在本文中,我将探讨 .NET Framework 3.5 版本中与 XML 和 LINQ 相关的某些功能。当然,这并不是对这两个主题的详尽讨论,而只是一个熟悉和进一步学习和探索的起点。

LINQ 部分

在讨论 LINQ to XML 或 LINQ to 任何其他内容时,首先需要讨论的当然是 LINQ。

LINQ

语言集成查询 (Language-Integrated Query),简称 LINQ,是 .NET Framework 3.5 的一项扩展,它使查询和集合操作成为 C# 等 .NET 语言的一等公民。它被进一步定义为“一套通用的标准查询运算符,允许以直接但声明式的方式在任何 .NET 语言中表达遍历、过滤和投影操作。”

入门

这是一个非常基本的 LINQ 查询示例

string[] names = new string[] { "John", "Paul", "George", "Ringo" };
var name = names.Select(s => s);

这里有两个值得注意的地方:var 关键字和看起来奇怪的运算符 =>

Var 关键字

var 是 3.5 版本中引入的新数据类型。虽然它看起来类似于 VB 或 JavaScript 中的 var 数据类型,但又不完全相同。在 VB 和 JavaScript 中,var 代表一种变体数据类型,可以用来表示几乎任何东西。然而,在 LINQ 中,var 更像是一个占位符;实际数据类型在编译时设置,并由其使用的上下文推断。

在上面的示例中,name 被解析为 System.Linq.Enumerable.SelectIterator<string,bool>

var name = "Hello, World"; 

但是,在此示例中,name 被解析为 string

这种模糊性在你不确定查询的确切返回值时非常有用,而且在不必要的情况下无需将变量转换为其他类型即可使用它,非常方便。

Lambda 表达式

Lambda 表达式最早由数学家 Alonzo Church 在 1936 年引入,作为表达算法的简写形式。在 .NET 3.5 中,它们是一种方便的方式,让开发人员可以定义可以作为参数传递的函数,并且是 .NET 2.0 中引入的匿名方法的演进。

=> 运算符用于分隔左侧的输入变量和右侧的表达式体。

string[] names = new string[] { "John", "Paul", "George", "Ringo" };
var name = names.Select(s => s.StartsWith("P"));

在此示例中,names 数组中的每个字符串都由变量 s 表示。由于数据类型可以从集合类型(在本例中为 names)推断出来,因此不必声明数据类型。

这两个语句在某种程度上是相似的

var name = names.Select(s => s);
foreach(string s in names) { }

表达式体 s.StartsWith("P") 只是使用字符串方法返回一个布尔值。Select 是一个扩展方法(稍后会详细介绍),它接受一个 Func 对象作为其参数。

Func 和 Action

FuncAction 是 .NET 3.5 中提供的两个新方法,用于表示委托。

Func<TSource, TResult>

这用于表示返回一个值 TResult 的委托。

Action<T>

另一方面,这用于表示不返回任何值的委托。

我们一直在使用的示例可以重写如下

Func<string, bool> pOnly = delegate(string s) { return s.StartsWith("P"); }; 
string[] names = new string[] { "John", "Paul", 
                                "George", "Ringo" }; 
var name = names.Select(pOnly);

序列

运行本文中的演示代码,您会注意到上面所有示例都不会返回单个值。相反,它们返回一个布尔值集合,指示输入集合中的每个元素是否匹配指定的表达式。在 LINQ 中,此集合称为序列。

如果我们想要匹配表达式的单个值,我们可以使用 Single 扩展方法。

string name = names.Single(pOnly); 

在此处请注意,name 变量的类型为 string。虽然我们仍然可以使用 var,但我们知道返回值是,或者应该是,一个 string

扩展方法

扩展方法是 .NET 3.5 的一项功能,它允许开发人员在不修改原始类代码的情况下向现有类添加功能。当您想提供额外功能但又无法访问代码库(例如使用第三方库时)时,这是一个有用的场景。

扩展方法是静态类中的静态方法。这些方法的第一个参数类型化为它所扩展的数据类型,并使用 this 修饰符。请注意,this 用作修饰符,而不是当前对象的引用。

public static class StringExtensions
{
   public static int ToInt(this string number)
   {
      return Int32.Parse(number);
   }
   public static string DoubleToDollars(this double number)
   {
      return string.Format("{0:c}", number);
   }
   public static string IntToDollars(this int number)
   {
      return string.Format("{0:c}", number);
   }
}

当该类编译时,.NET 会将其应用 System.Runtime.CompilerServices.Extension 属性,当它在范围内时,Intellisense 可以读取此信息并根据数据类型确定适用哪些方法。

正如我们在这里看到的,在第一个示例中,Intellisense 知道 ToInt 方法适用于 string,而 DoubleToDollars 只适用于 double

查询表达式和方法

有两种执行 LINQ 查询的方式:查询表达式和点表示法。前者类似于 SQL 查询,只是 select 子句在最后。后者也分为两个:匿名方法和方法链。

string[] camps = new string[]{"CodeCamp2007","CodeCamp2008","CodeCamp2009"};
var currentCamp = from camp in camps
   where camp.EndsWith(DateTime.Now.Year.ToString())
   select camp;
string currentCamp = camps.Single(c => c.EndsWith(DateTime.Now.Year.ToString()));

这两个语句产生相同的结果,因为查询表达式格式在编译时会转换为方法。有几种方法可以生成结果。以下每种方法都会产生相同的结果。

string currentCamp2 = camps.Where(c => c.EndsWith(DateTime.Now.Year.ToString())).Single();
string currentCamp3 = camps.Single(c => c.EndsWith(DateTime.Now.Year.ToString()));
string currentCamp4 = camps.Select(c => c).Where(
       c => c.EndsWith(DateTime.Now.Year.ToString())).Single(); 

XML 部分

现在我们已经对 LINQ 有了了解,是时候转向 XML 部分了。

对于本文,我们将使用此 XML 文件

<?xml version="1.0" encoding="utf-8" ?>
<employees>
   <employee id="1" salaried="no">
      <name>Gustavo Achong</name>
      <hire_date>7/31/1996</hire_date>
   </employee>
   <employee id="3" salaried="yes">
      <name>Kim Abercrombie</name>
      <hire_date>12/12/1997</hire_date>
   </employee>
   <employee id="8" salaried="no">
      <name>Carla Adams</name>
      <hire_date>2/6/1998</hire_date>
   </employee>
   <employee id="9" salaried="yes">
      <name>Jay Adams</name>
      <hire_date>2/6/1998</hire_date>
   </employee>
</employees> 

旧方法

在以前版本的 .NET Framework 中,XML 是以文档为中心的;换句话说,要创建任何结构,您必须首先从 XMLDocument 开始。

public class OldWay
{
   private static XmlDocument m_doc = new XmlDocument();
   
   public static void CreateEmployees()
   {
      XmlElement root = m_doc.CreateElement("employees");
      root.AppendChild(AddEmployee(1, "Gustavo Achong", 
                       DateTime.Parse("7/31/1996"), false));
      root.AppendChild(AddEmployee(3, "Kim Abercrombie", 
                       DateTime.Parse("12/12/1997"), true));
      root.AppendChild(AddEmployee(8, "Carla Adams", 
                       DateTime.Parse("2/6/1998"), false));
      root.AppendChild(AddEmployee(9, "Jay Adams", 
                       DateTime.Parse("2/6/1998"), false));
      m_doc.AppendChild(root);
      Console.WriteLine(m_doc.OuterXml);
   }

   private static XmlElement AddEmployee(int ID, string name, 
                  DateTime hireDate, bool isSalaried)
   {
      XmlElement employee = m_doc.CreateElement("employee");
      XmlElement nameElement = m_doc.CreateElement("name");
      nameElement.InnerText = name;
      XmlElement hireDateElement = m_doc.CreateElement("hire_date");
      hireDateElement.InnerText = hireDate.ToShortDateString();
      employee.SetAttribute("id", ID.ToString());
      employee.SetAttribute("salaried", isSalaried.ToString());
      employee.AppendChild(nameElement);
      employee.AppendChild(hireDateElement);
      return employee;
   }
}

聪明的开发人员会创建辅助方法来减轻痛苦,但这仍然是一个冗长、繁琐的过程。XMLElement 无法单独创建,它必须从 XMLDocument 创建。

XmlElement employee = m_doc.CreateElement("employee"); 

尝试这样做会产生编译器错误

XmlElement employee = new XmlElement(); 

查看上面的示例,也很难了解此文档的架构。

新方法

使用 System.Xml.Linq 命名空间中的类以及 .NET 3.5 中可用的功能,构造 XML 文档非常简单且易于阅读。

public static void CreateEmployees()
{
   XDocument doc = new XDocument(
      new XDeclaration("1.0", "utf-8", "yes"),
      new XComment("A sample xml file"),
      new XElement("employees",
         new XElement("employee",
            new XAttribute("id", 1),
            new XAttribute("salaried", "false"),
               new XElement("name", "Gustavo Achong"),
               new XElement("hire_date", "7/31/1996")),
         new XElement("employee",
            new XAttribute("id", 3),
            new XAttribute("salaried", "true"),
               new XElement("name", "Kim Abercrombie"),
               new XElement("hire_date", "12/12/1997")),
         new XElement("employee",
            new XAttribute("id", 8),
            new XAttribute("salaried", "false"),
               new XElement("name", "Carla Adams"),
               new XElement("hire_date", "2/6/1998")),
         new XElement("employee",
            new XAttribute("id", 9),
            new XAttribute("salaried", "false"),
               new XElement("name", "Jay Adams"),
               new XElement("hire_date", "2/6/1998"))
       )
   );
}

以这种方式构造文档之所以可能,是因为 LINQ to XML 中的函数式构造功能。函数式构造只是在单个语句中创建整个文档树的一种方法。

public XElement(XName name, Object[] content)

正如我们从 XElement 的一个构造函数中看到的,它接受一个对象数组。在上面的示例中,employees 元素由四个 XElement 构造,每个员工一个,而每个 XElement 又由 XAttribute 和其他 XElement 构造。

在上面的示例中,如果我们删除了 XDeclarationXComment 对象,可以将 XDocument 替换为 XElement。这是因为 XDocument 使用的构造函数接受一个 XDeclaration 实例,而不是 XElement 构造函数接受的 XName

public XDocument(XDeclaration declaration,Object[] content) 

运行演示时还要注意的一点是,两个文档都打印到控制台窗口。

正如我们所见,旧方法只是将文档的内容流式传输到控制台。该方法也这样做;但是,它格式精美,无需额外努力。

命名空间支持

当然,通过 XNamespace 类支持命名空间。

XNamespace ns = http://mycompany.com;
XElement doc = new XElement(
   new XElement(ns + "employees",
   new XElement("employee", 

需要注意的一点是,如果一个元素使用了命名空间,那么所有元素都必须使用命名空间。在上面的情况中,我们可以看到一个空的 xmlns 属性将被添加到 employee 元素中。

<employees xmlns="http://mycompany.com">
   <employee id="1" salaried="false" xmlns="">

显式转换

新的 XML 支持的众多优点之一是对值进行显式转换的支持。

以前,所有 XML 值都被视为字符串,并且需要根据需要进行转换。

// Must be string, or converted to string
//idElement.InnerText = 42;
idElement.InnerText = "42";
int id = Convert.ToInt32(idElement.Value); 

使用新的 API,这更加直观

XElement element1 = new XElement("number", 42);
// It doesn't matter it the value is a string or int
XElement element2 = new XElement("number", "42");

int num1 = (int)element1;
int num2 = (int)element2; 

遍历 XML 树

遍历 XML 树仍然非常容易。

foreach(var node in doc.Nodes()) 

我们可以使用文档或根元素集合中的节点。但请注意,这将遍历整个树,包括所有子节点,而不仅仅是同级节点。

foreach(var node in doc.Nodes().OfType<XComment>()) 

此方法可用于遍历特定类型的节点,在本例中是注释。或者我们可以通过这种方式访问特定子节点。

foreach(var node in doc.Elements("employees").Elements("employee").Elements("name")) 

这比嵌套迭代或使用 XPath 查询获取 XMLNodeList 是一种改进。

XPath

XPath 支持已通过扩展方法内置到 API 中,例如

  • Descendents
  • Ancestors
  • DescendentsAndSelf
  • AncestorsAndSelf
  • ElementsBeforeSelf
  • ElementsAfterSelf

这不是一个详尽的列表,所以请查阅文档以了解所有其他可用选项。

转换 XML

使用我们都熟悉的那些方法,仍然可以转换 XML 文档或元素。

//Load the stylesheet.
XslTransform xslt = new XslTransform();
xslt.Load(stylesheet);

//Load the file to transform.
XPathDocument doc = new XPathDocument(filename);
//Create an XmlTextWriter which outputs to the console.

XmlTextWriter writer = new XmlTextWriter(Console.Out);

//Transform the file and send the output to the console.
xslt.Transform(doc, null, writer, null);
writer.Close(); 

然而,使用新的 API,我们可以利用函数式构造和 LINQ 查询来转换文档

XElement element = new XElement("salaried_employees", from e in doc.Descendants("employee") 
          where e.Attribute("salaried").Value == "true" 
          select new XElement("employee", 
             new XElement(e.Element("name")) ) ); 

结论

XML 是一种出色的构造,它已深深植根于几乎所有事物之中。能够轻松构建、查询、转换和操作 XML 文档是一项宝贵的服务,它将提高应用程序的构建速度和应用程序的质量。

本文并不是对 LINQ to XML 的详尽调查;关于这个主题已经有许多其他文章、代码片段和博客。它主要只是展示了 .NET 3.5 可能实现的用途并帮助您熟悉它们。

参考文献

历史

  • 首次发布:2008 年 3 月 14 日。
© . All rights reserved.