用XDocument读取XML文件最稳妥,直接用XDocument.Load()加载本地文件或流,它自动识别BOM和声明编码,对UTF-8/BOM、换行缩进不敏感,且支持强类型查询;避免用File.ReadAllText()后手动解析,以防绕过校验抛XmlException。

用 XDocument 读取 XML 文件最稳妥
直接用 XmlDocument 容易在编码、命名空间、空白节点上翻车,XDocument(LINQ to XML)更符合现代 C# 习惯,对 UTF-8/BOM、换行缩进不敏感,且支持强类型查询。
常见错误:用 File.ReadAllText() 再手动解析字符串——这绕过了 XML 解析器的校验,遇到非法字符或编码不匹配会直接抛 XmlException。
- 始终用
XDocument.Load("path.xml"),它自动识别 BOM 和声明中的encoding - 若路径含中文或特殊字符,确保文件存在且进程有读取权限;网络路径需用
XDocument.Load(new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore })防止外部实体攻击 - 读取失败时,
XDocument.Parse(string)仅用于已确认格式合法的字符串片段,别用来“救”损坏的文件
修改节点值前先定位到具体 XElement
XML 修改不是“找到标签名就改”,必须明确作用域:是第一个 <name></name>?某个 <user id="102"></user> 下的子节点?还是所有同名节点?漏掉 .FirstOrDefault() 或误用 .Elements() 而非 .Descendants() 是高频出错点。
示例场景:把 <config><timeout>30</timeout></config> 改成 60:
XDocument doc = XDocument.Load("config.xml");
XElement timeoutNode = doc.Root?.Element("Timeout"); // Root 是 <Config>
if (timeoutNode != null)
{
timeoutNode.Value = "60"; // 直接赋值,不拼接标签
}- 用
.Element("TagName")查直接子元素,用.Descendants("TagName")查任意层级——后者可能返回多个,务必加.FirstOrDefault()或遍历 - 修改文本内容用
.Value,不是.InnerText(XElement没这属性) - 如果节点不存在,
.Element()返回null,不抛异常——这是安全的设计,但你要自己判空
保存时注意写入权限和原子性
doc.Save("path.xml") 是覆盖写入,没有备份、不检查文件是否被其他进程占用。线上配置文件一旦写坏,服务可能启动失败。
- 先写入临时文件(如
"config.xml.tmp"),再用File.Replace()原子替换,避免中间态损坏 - 确保目标目录可写;若 XML 在程序目录下,.NET 6+ 的单文件发布模式中
AppContext.BaseDirectory可能是只读的,得改用Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) - 不要用
StreamWriter手动写字符串——会丢失缩进、声明头、编码声明,导致下次加载失败
增删节点比改值更容易出错
新增节点时,new XElement("Tag", "value") 创建的是独立对象,必须显式挂载到父节点(parent.Add() 或 parent.AddFirst()),否则内存里存在但没写入文档树。
删除节点时,调用 node.Remove(),不是 node = null——后者只是让变量失效,原节点还在文档中。
- 添加属性用
new XAttribute("id", "102"),传给XElement构造函数或调用.Add() - 插入新节点后,若需保持原有格式缩进,调用
doc.Save(path, SaveOptions.DisableFormatting)关闭自动格式化,否则可能破坏人工排版 - 批量修改多个同名节点?用
doc.Descendants("Item").ToList().ForEach(x => x.Value = "new"),注意.ToList()防止集合被修改时迭代异常
实际项目里,XML 很少单独作为核心数据载体,更多是配置或导出交换格式。真正难的不是语法,而是厘清“这个节点到底属于哪个上下文”——比如 <name></name> 在 <user></user> 下和在 <group></group> 下语义完全不同,查错时容易跳过命名空间或父级条件。


















