LINQ to XML并不是一套独立于LINQ的复杂框架,而是System.Xml.Linq命名空间下的一组类型。它把XML文档转换成由XDocument、XElement、XAttribute组成的对象树,这些对象都实现了IEnumerable接口,所以可以直接用标准LINQ扩展方法做筛选、投影、排序和聚合。相比XmlDocument的命令式遍历,它的代码通常更短,也更接近我们对查询逻辑的自然描述。

一、先理解LINQ to XML的对象模型和加载方式
接触LINQ to XML需要先分清三个核心类型:XDocument代表整份XML文档,XElement代表一个元素节点,XAttribute代表元素上的属性。它们都可以通过构造函数直接创建,也可以从文件、URL、流或字符串加载。和传统XmlDocument最大的不同是,这些类型实现了IEnumerable<T>,因此可以无缝接入LINQ扩展方法。
加载XML通常使用XDocument.Load读取路径或流,使用XDocument.Parse解析字符串。加载完成后,Root属性指向根元素。对于元素访问,Element只查找直接子元素,Elements返回所有直接子元素,Descendants则会递归查找所有层级。下面这段代码演示了从字符串解析XML,并统计根节点下的直接子元素数量:
using System;
using System.Linq;
using System.Xml.Linq;
class Program
{
static void Main()
{
string xml = @"<Books>
<Book id=""1"">
<Title>C#入门经典</Title>
<Price>59.00</Price>
</Book>
<Book id=""2"">
<Title>LINQ实战</Title>
<Price>79.00</Price>
</Book>
</Books>";
XDocument doc = XDocument.Parse(xml);
XElement root = doc.Root;
Console.WriteLine(root.Name.LocalName);
Console.WriteLine(root.Elements("Book").Count());
}
}
注意XDocument.Parse适合内容长度可控的场景。如果XML来自磁盘文件,优先用Load,它会自动处理文件流和编码声明。若XML体量很大,比如几百MB的日志文件,建议改用XmlReader配合XElement.Load逐个读取节点,而不是一次性加载整个文档。
二、用LINQ表达式完成节点筛选、投影与分组
日常查询中,最常用的入口是Descendants("Book"),它会返回文档中所有名为<Book>的元素。结合Where可以做条件过滤,Select可以投影成匿名对象。由于XElement和XAttribute内置了显式类型转换,我们可以直接写成(string)b.Attribute("id"),节点不存在时返回null,而不会像.Value那样抛出空引用异常。
下面这个例子找出id为1的图书,并按照价格降序输出。这里把XML节点转换为强类型属性后,后续排序、比较都变得直观:
var books = doc.Descendants("Book")
.Where(b => (string)b.Attribute("id") == "1")
.Select(b => new
{
Id = (string)b.Attribute("id"),
Title = (string)b.Element("Title"),
Price = (decimal)b.Element("Price")
})
.OrderByDescending(b => b.Price)
.ToList();
foreach (var book in books)
{
Console.WriteLine($"编号:{book.Id},书名:{book.Title},价格:{book.Price}");
}
如果要对结果分组统计,可以用GroupBy配合Average、Count等方法。比如按分类统计图书数量和平均价格,代码同样不需要额外循环:
var categoryGroups = doc.Descendants("Book")
.GroupBy(b => (string)b.Element("Category"))
.Select(g => new
{
Category = g.Key,
Count = g.Count(),
AvgPrice = g.Average(b => (decimal)b.Element("Price"))
})
.OrderByDescending(g => g.Count);
foreach (var group in categoryGroups)
{
Console.WriteLine($"{group.Category}:{group.Count} 本,平均价格 {group.AvgPrice:F2}");
}
实际开发里,建议把查询结果投影成DTO或匿名对象后,再做业务处理。直接拿着XElement到处传递,虽然省事,但会让数据访问逻辑和业务逻辑耦合在一起。投影成对象后,也更方便单元测试和后续序列化。
三、用LINQ to XML修改XML并保存
除了查询,LINQ to XML也支持完整的增删改操作。创建节点用new XElement,添加属性用new XAttribute,然后通过Add挂到父节点。修改属性建议使用SetAttributeValue,修改元素文本可以直接给Value赋值。删除节点使用Remove,可以作用于单个节点,也可以作用于查询返回的集合。
下面的代码会在原文档中追加一本新书,修改id为2的图书信息,并删除价格低于60的图书。所有操作都作用在内存中的对象树上,最后调用Save才会写回磁盘:
XElement root = doc.Root;
XElement newBook = new XElement("Book",
new XAttribute("id", "3"),
new XElement("Title", "深入理解C#"),
new XElement("Price", "89.00")
);
root.Add(newBook);
XElement target = root.Elements("Book")
.FirstOrDefault(b => (string)b.Attribute("id") == "2");
if (target != null)
{
target.SetAttributeValue("id", "20");
target.Element("Title").Value = "LINQ实战(修订版)";
}
root.Elements("Book")
.Where(b => (decimal)b.Element("Price") < 60)
.Remove();
doc.Save("books.xml", SaveOptions.DisableFormatting);
这里有一个容易忽略的细节:Value赋值时会替换节点的所有子节点,只保留纯文本。如果节点内部还包含子元素,直接赋值Value会把这些子元素清空。需要保留混合内容时要谨慎。此外,保存时使用SaveOptions.DisableFormatting可以去掉多余缩进,适合网络传输;不带该选项则默认生成带缩进的易读XML。
四、命名空间处理和常见避坑点
XML命名空间是实际项目中经常遇到的障碍。例如文档声明了默认命名空间xmlns="http://ipipp.com/books",此时直接调用Descendants("Book")是查不到节点的,因为元素的全名已经变成{http://ipipp.com/books}Book。正确做法是创建XNamespace对象,再用ns + "Book"拼接查询名:
XNamespace ns = "http://ipipp.com/books";
var books = doc.Descendants(ns + "Book")
.Where(b => (string)b.Attribute("id") != null)
.Select(b => new
{
Id = (string)b.Attribute("id"),
Title = (string)b.Element(ns + "Title")
})
.ToList();
如果XML结构会频繁使用命名空间,也可以先用对象构造器创建带命名空间的文档,保证查询和写入统一:
XNamespace ns = "http://ipipp.com/books";
XDocument doc = new XDocument(
new XElement(ns + "Books",
new XElement(ns + "Book",
new XAttribute("id", 1),
new XElement(ns + "Title", "C#编程指南")
)
)
);
doc.Save("books.xml");
最后再强调几个容易踩的坑:第一,XElement.Value会把当前元素下所有文本拼接起来,如果只想要直接文本,需要访问Nodes().OfType<XText>();第二,显式转换(int)element在节点不存在或格式不正确时会抛出异常,而(int?)element和(string)element在节点缺失时返回null,更适合容错处理;第三,不要用字符串拼接的方式手动生成XML,这样既容易产生转义问题,也会丢失结构信息,直接使用对象构造器或XElement.Parse解析完整片段才是安全做法。
掌握这些基本操作后,处理配置文件、RSS订阅、SOAP响应或单元测试中的XML数据,都能明显减少模板代码。LINQ to XML的价值不只是少写几个循环,而是让XML操作重新回到强类型、可组合的编程模型里。
LINQ to XMLXML查询C# XML解析修改时间:2026-10-03 05:42:33