LINQ to XML
对 .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
Func
和 Action
是 .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
构造。
在上面的示例中,如果我们删除了 XDeclaration
和 XComment
对象,可以将 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 可能实现的用途并帮助您熟悉它们。
参考文献
- http://msdn2.microsoft.com/en-us/library/bb308960.aspx
- http://www.hookedonlinq.com/LINQOverview.ashx
历史
- 首次发布:2008 年 3 月 14 日。