XML反序列化仅识别首个嵌套对象标签的问题排查
问题
我有以下C#类:
public interface IVariable { string Name { get; } dynamic Value { get; set; } } public class Namespace : IVariable, IXmlSerializable, IEnumerable<IVariable> { protected Namespace() { } public Namespace(string name, params string[] aliases) { this.Name = name; this._aliases.AddRange(aliases); } private readonly List<IVariable> _variables = new List<IVariable>(); public string Name { get; private set; } dynamic IVariable.Value { get => this; set => throw new InvalidOperationException(); } private readonly List<string> _aliases = new List<string>(); public void Add(IVariable variable) { _variables.Add(variable); } public dynamic this[string key] { get { foreach (var variable in _variables) { if (variable.Name == key) return variable.Value; if (variable is Namespace ns && ns._aliases.Contains(key)) return ns; } return null; } set { foreach (var variable in _variables) { if (variable.Name == key) variable.Value = value; if (variable is Namespace ns && ns._aliases.Contains(key)) throw new InvalidOperationException(); } } } void IXmlSerializable.WriteXml(XmlWriter writer) { writer.WriteAttributeString("name", Name); foreach (var alias in _aliases) { writer.WriteElementString("Alias", alias); } foreach (var variable in _variables) { if (variable is Variable v) { writer.WriteStartElement("Variable"); writer.WriteAttributeString("name", v.Name); writer.WriteAttributeString("typecode", v.TypeCode.ToString()); writer.WriteString(v.Value.ToString()); writer.WriteEndElement(); } else { var serializer = new XmlSerializer(typeof(Namespace)); serializer.Serialize(writer, (Namespace)variable); } } } XmlSchema IXmlSerializable.GetSchema() { return null; } void IXmlSerializable.ReadXml(XmlReader reader) { Name = reader.GetAttribute("name"); var aliases = new List<string>(); while (true) { reader.Read(); if (reader.Name != "Alias") break; reader.Read(); _aliases.Add(reader.Value); reader.Read(); } while (true) { if (reader.Name == "Variable") { var name = reader.GetAttribute("name"); var typecode = (TypeCode)Enum.Parse(typeof(TypeCode), reader.GetAttribute("typecode")); reader.Read(); var variable = new Variable(typecode, name); variable.SetValueFromString(reader.Value); Add(variable); } else if (reader.Name == "Namespace") { var serializer = new XmlSerializer(typeof(Namespace)); _variables.Add((Namespace)serializer.Deserialize(reader)); } else { break; } } } public IEnumerator<IVariable> GetEnumerator() { return ((IEnumerable<IVariable>)_variables).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_variables).GetEnumerator(); } }
Namespace可以包含Variable或其他Namespace,Variable能存储System.TypeCode定义的多种类型,我需要保存整个嵌套结构。WriteXml方法工作正常,能生成符合预期的XML:
void Main() { var stream = new MemoryStream(); var serializer = new XmlSerializer(typeof(Namespace)); var ns = new Namespace("foo", "f") { new Variable(TypeCode.String, "bar", "baz"), new Namespace("qux") { new Variable(TypeCode.Int32, "quxx", 42) }, }; serializer.Serialize(stream, ns); stream.Position = 0; Console.Out.WriteLine(new StreamReader(stream).ReadToEnd()); }
生成的XML如下:
<?xml version="1.0" encoding="utf-8"?> <Namespace name="foo"> <Alias>f</Alias> <Variable name="bar" typecode="String">baz</Variable> <Namespace name="qux"> <Variable name="quxx" typecode="Int32">42</Variable> </Namespace> </Namespace>
但反序列化时:
stream.Position = 0; var ns2 = (Namespace)serializer.Deserialize(stream); foreach (IVariable variable in ns2) { Console.Out.WriteLine(variable.Name); }
我只能获取到第一个成员(比如bar),如果交换Variable和Namespace的定义顺序,就只能获取到Namespace名称而非Variable。我尝试在while循环体前后添加reader.Read()调用,却导致反序列化认为XML损坏。有意思的是,我可以添加任意数量的<Alias>标签而不会有问题。请问我哪里出错了?
解决方案
问题出在ReadXml方法的第二个while循环里——处理完第一个元素后,你没有正确移动XmlReader的指针到下一个元素,导致循环无法识别后续节点,直接跳出。
具体原因:
- 处理
<Variable>后,XmlReader停留在</Variable>节点上,此时reader.Name仍为"Variable",但没有调用reader.Read()移动到下一个元素的起始标签,下一次循环会重复处理已结束的节点,最终触发else分支跳出。 - 处理
<Namespace>时,serializer.Deserialize(reader)会自动将指针移到</Namespace>之后,但循环没有先调用reader.Read()定位到下一个元素的起始标签,导致reader.Name不是预期的元素名,直接跳出循环。
修改后的ReadXml方法如下:
void IXmlSerializable.ReadXml(XmlReader reader) { Name = reader.GetAttribute("name"); // 读取Alias节点 while (reader.Read()) { if (reader.NodeType == XmlNodeType.Element && reader.Name == "Alias") { reader.Read(); _aliases.Add(reader.Value); // 移动到</Alias>节点 reader.Read(); } else { // 跳出Alias循环,准备读取Variable/Namespace break; } } // 读取Variable和Namespace节点 while (true) { // 先移动到下一个元素节点,若不是元素则退出循环 if (!reader.Read() || reader.NodeType != XmlNodeType.Element) break; if (reader.Name == "Variable") { var name = reader.GetAttribute("name"); var typecode = (TypeCode)Enum.Parse(typeof(TypeCode), reader.GetAttribute("typecode")); reader.Read(); var variable = new Variable(typecode, name); variable.SetValueFromString(reader.Value); Add(variable); // 移动到</Variable>节点 reader.Read(); } else if (reader.Name == "Namespace") { var serializer = new XmlSerializer(typeof(Namespace)); _variables.Add((Namespace)serializer.Deserialize(reader)); // Deserialize已自动将指针移到</Namespace>之后,无需手动Read } else { // 跳过未知元素,提升鲁棒性 reader.Skip(); } } }
关键修改点:
- 第一个循环改用
reader.Read()作为条件,同时判断节点类型为XmlNodeType.Element,避免误判非元素节点。 - 第二个循环开头先调用
reader.Read()定位到下一个有效元素节点,确保每次循环处理的都是起始标签。 - 处理完
<Variable>后,手动调用reader.Read()移动到闭合标签之后,为下一次循环做准备。 - 增加未知元素的跳过逻辑
reader.Skip(),避免因意外节点导致反序列化失败。
修改后,反序列化就能正确读取所有嵌套的Variable和Namespace节点了。
内容的提问来源于stack exchange,提问作者RoadieRich
相关产品推荐
相关产品推荐

