在C#开发中,处理XML数据是家常便饭。但不少开发者一上来就踩坑,要么用XmlDocument这种老古董,要么硬写Elements()链式调用,结果遇到注释、空格或命名空间就挂掉。其实,XDocument搭配Descendants()才是稳妥的起点,它能穿透所有层级,跳过非元素节点,稳定可靠。

简单来说,XDocument + Descendants() 是最稳的起点,别碰 XmlDocument 或硬写 Elements() 链式调用——前者过时,后者一遇到注释、空格、命名空间就失效。
用 Descendants() 查任意层级节点,不依赖结构深度
XML 常有注释、处理指令、换行文本节点,Elements() 只查直接子节点,一旦 Root 下多了一行空格或一个 ,doc.Root.Elements("Item") 就返回空。而 Descendants("Item") 会穿透所有层级,跳过非元素节点,结果稳定。
- 始终从
XDocument.Load("file.xml")或XDocument.Parse(xmlString)开始,不是XmlDocument - 别写
doc.Root.Descendants("Product")——Root可能为null(比如 XML 声明后直接是注释),直接用doc.Descendants("Product") - 如果只查顶层同名节点(如根下所有
Book),且确定结构干净,Elements()略快;但日常开发中,容错比这点性能重要得多
带默认命名空间的 XML 必须声明 XNamespace
像 这种,doc.Descendants("item") 永远为空——字符串匹配对不上命名空间隐式前缀。
- 先提取命名空间:
XNamespace ns = doc.Root?.GetDefaultNamespace() ?? ""; - 再查询:
doc.Descendants(ns + "item") - 若 XML 有多个命名空间(如
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"),用doc.Root?.GetNamespaceOfPrefix("xsi")获取对应XNamespace - 漏掉这步,所有
Element()、Attribute()、Where()都查不到东西,错误静默,极难排查
用 Where() + Attribute() / Element() 写条件,别字符串匹配
想查 ?别写 .Where(x => x.ToString().Contains("shipped"))——它会把整个节点序列化成字符串再搜,慢、不准、还可能误中子节点内容。
- 查属性值:
doc.Descendants("order").Where(x => x.Attribute("status")?.Value == "shipped") - 查子元素文本:
doc.Descendants("book").Where(x => x.Element("Author")?.Value == "Jon Skeet") - 查存在性(不取值):
doc.Descendants("book").Where(x => x.Element("Price") != null) Attribute()和Element()返回XAttribute/XElement?,安全调用?.Value,避免NullReferenceException
取值前强制转换 + 判空,别信 Value 永远有值
Element("Title").Value 在节点不存在时抛 NullReferenceException;Attribute("Id").Value 同理。LINQ to XML 不自动补默认值。
- 安全取字符串:
(string)node.Element("Title")—— 转换失败返回null,不抛异常 - 安全取数字:
(int?)node.Attribute("Id"),null表示缺失或解析失败 - 批量判空用
.Any():if (doc.Descendants("Book").Any()) { ... },比.Count() > 0快(不用遍历全部) - 性能敏感场景(如循环内多次查同一节点),把
doc.Descendants("Book")结果缓存为IEnumerable或List,避免重复遍历
命名空间和空值处理是实际项目里最常卡住人的两点,其他都好调——但一旦 XML 带了 xmlns,或者某条数据缺了 Author 字段,没做安全转换的代码就会在生产环境突然崩掉。