DataSet.ReadXml()是.NET中一个看起来简单、实际坑不少的方法。它的核心思路是把XML文档自动推断成若干张DataTable,再用DataRelation把父子表关联起来。对于结构规整的XML,一行代码就能搞定读取;但一旦XML里出现嵌套层级、混合属性和元素、同名节点在不同位置出现等情况,推断结果就会变得难以预料。本文结合几个典型结构,把ReadXml的推断规则讲清楚,并给出处理复杂XML的实用方案。

ReadXml的推断机制:节点如何变成表和列
ReadXml在默认情况下使用XmlReadMode.Auto,也就是由.NET自己去"猜"XML结构。推断规则大致是:有子元素的节点被推断为表,只有文本内容的节点被推断为列,属性一律被推断为列,重复出现的兄弟节点会触发新表或者关系。理解这套规则,是判断复杂XML能不能直接用ReadXml的第一步。
看一个典型的订单XML,它包含订单头、多个订单行、以及商品信息三层嵌套:
<Orders>
<Order OrderId="1001">
<Customer>张三</Customer>
<OrderDate>2024-05-01</OrderDate>
<Items>
<Item ItemNo="A01">
<Product>机械键盘</Product>
<Qty>2</Qty>
</Item>
<Item ItemNo="A02">
<Product>无线鼠标</Product>
<Qty>3</Qty>
</Item>
</Items>
</Order>
</Orders>用下面的代码读取并查看推断出的结构:
DataSet ds = new DataSet();
ds.ReadXml("orders.xml");
foreach (DataTable table in ds.Tables)
{
Console.WriteLine("表名: " + table.TableName);
foreach (DataColumn col in table.Columns)
{
Console.WriteLine(" 列: " + col.ColumnName + " 类型: " + col.DataType.Name);
}
}
// 输出通常是三张表:Order、Item,其中Items节点因只有一个重复子节点
// 会被折叠掉,Item表自动生成Order_Id外键列关联到Order表这个例子能顺利读取,是因为结构满足推断规则的理想形态:根节点下的Order有子元素所以成为表,CustomerId和OrderDate成为Order表的列,属性OrderId也进列,Item重复出现自然成表,并且自动加上外键。可以通过ds.Relations检查自动建立的关系,取子表数据时配合GetChildRows使用:
DataTable orderTable = ds.Tables["Order"];
foreach (DataRow order in orderTable.Rows)
{
Console.WriteLine("订单: " + order["OrderId"] + ", 客户: " + order["Customer"]);
foreach (DataRow item in order.GetChildRows("Order_Item"))
{
Console.WriteLine(" 商品: " + item["Product"] + " x " + item["Qty"]);
}
}三种让推断失败或结果混乱的典型结构
第一种是混合内容节点。如果某个节点既有文本又有子元素,推断机制会无所适从,文本内容可能被丢弃或者结构完全变形。例如<Remark>加急<Tag>VIP</Tag>处理</Remark>这种节点,ReadXml读出来的结果往往不是你想要的。这种XML本质上更适合用XmlDocument或XDocument手动处理,文本和子节点才能分别取到。
第二种是同一节点名在不同层级出现。假设根下有个<Name>节点是公司名,Order下也有个<Name>节点是下单人姓名,推断时两处会被合并进同一张表或者产生意料之外的关联,导致数据串行。解决思路是先读取再手动调整表结构,或者干脆在读取前用XDocument做一次预处理,把不同层级的同名节点改名,再交给ReadXml处理:
XDocument doc = XDocument.Load("input.xml");
// 给订单内的Name加上前缀,避免和公司级Name冲突
foreach (var el in doc.Descendants("Order").Elements("Name"))
{
el.Name = XName.Get("OrderName");
}
doc.Save("normalized.xml");
DataSet ds = new DataSet();
ds.ReadXml("normalized.xml");第三种是深层嵌套超过两层且中间层是"包装节点"。有些XML为了语义清晰会有多层包装,比如Order下有Shipping再嵌Address再嵌City。ReadXml会为每一层都生成一张表,表数量膨胀,查询要跨好几层关系,写起来非常繁琐。针对这种情况,更合理的做法是放弃自动推断,改用XmlSerializer配合自定义类反序列化,类的属性直接映射XML结构,可读性和维护性都好得多。
用XmlReadMode和XML Schema控制读取行为
很多读取问题其实源于让.NET去猜。ReadXml提供了一个更可靠的路径:先给XML配套一份XSD Schema,再用XmlReadMode.ReadSchema读取。有了Schema,表名、列名、类型、关系全部是确定的,推断环节的种种不确定性直接消失。可以从Visual Studio的"XML"菜单里基于样例XML生成Schema,也可以调用WriteXmlSchema让符合预期的DataSet反向导出Schema做模板:
DataSet ds = new DataSet();
ds.ReadXmlSchema("orders.xsd"); // 先加载结构
ds.ReadXml("orders.xml", XmlReadMode.ReadSchema); // 再按结构填充数据XmlReadMode的几个常用值需要分清。IgnoreSchema会跳过内联Schema,遇到不匹配的数据直接丢弃;InferSchema在只有数据没有结构时推断并合并;DiffGram用于读取DataSet导出的增量数据,普通业务场景很少用到;Fragment适合读取XML片段而非完整文档。另外要注意,如果XML文件本身内嵌了Schema定义,Auto模式会优先采用它,这也是某些文件读取结果和预期不一致的原因之一。
还有一个容易被忽略的细节:ReadXml对XML中的空节点和属性节点的处理。一个空的自闭合节点<Note/>会推断为表还是列,取决于它在整个文档中其他位置的表现,单看一个文件片段是判断不准的。因此强烈建议在开发阶段把推断结果打印出来核对一遍,确认表结构、外键列名都符合预期后再写业务代码,避免上线后遇到格式变化的XML文件时程序莫名出错。
什么时候应该放弃ReadXml换其他方案
判断标准其实很直接:XML结构是否稳定、层级是否规整、是否有Schema。三者都满足时,ReadXml配合强类型DataSet是效率很高的方案,读完就能绑定DataGridView或者直接用LINQ to DataSet查询。反之,只要出现混合内容、同名节点、超过三层的深层嵌套、或者需要按条件只取部分数据,就应该考虑XDocument的LINQ查询:
var orders = from o in XDocument.Load("orders.xml")
.Descendants("Order")
where (string)o.Element("Customer") == "张三"
select new
{
Id = (string)o.Attribute("OrderId"),
Items = o.Descendants("Item").Select(i => new
{
Product = (string)i.Element("Product"),
Qty = (int)i.Element("Qty")
}).ToList()
};XDocument的好处是无论结构多复杂,取值路径都是显式声明的,不会受推断机制影响。XmlSerializer则适合XML结构和业务对象一一对应的场景,定义好类加几个特性标注就能双向转换。三种方案没有绝对优劣:ReadXml胜在自动成表、便于数据绑定;XDocument胜在灵活精确;XmlSerializer胜在类型安全。理解了ReadXml背后那套推断规则,你就能准确判断手里的XML该走哪条路,而不是遇到问题盲目换方法试错。
DataSet.ReadXmlXML解析C#读取XML修改时间:2026-09-03 10:21:34