XML 使用代码默认值反序列化集合 属性

XML Deserialization of collection property with code defaults

对于应用程序配置,我经常会创建一个配置 class,其中包含应用程序的配置值,然后将其反序列化为要使用的对象。配置对象通常数据绑定到用户界面控件,以便用户可以更改和保留配置。配置 class 通常具有分配给属性的默认值,因此始终存在默认配置。这很有效。我最近遇到一种情况,我有一个提供一些默认路径信息的字符串列表。我所看到的让我意识到我并不完全知道在 XML 反序列化到对象期间如何填充对象属性。

所以我创建了一个简单的示例来展示该行为。下面是一个简单的 class,它有几个具有一些代码默认值的属性。

[Serializable]
public class TestConfiguration
   {
      public String Name 
      { 
         get
         {
            return mName;
         }
         set
         {
            mName = value;
         }
      }private String mName = "Pete Sebeck";

  public List<String> Associates 
  { 
     get
     {
        return mAssociates;
     }
     set
     {
        mAssociates = value;
     }
  } private List<String> mAssociates = new List<string>() { "Jon", "Natalie" };

  public override String ToString()
  {
     StringBuilder buffer = new StringBuilder();
     buffer.AppendLine(String.Format("Name: {0}", Name));
     buffer.AppendLine("Associates:");
     foreach(String associate in mAssociates)
     {
        buffer.AppendLine(String.Format("\t{0}", associate));
     }
     return buffer.ToString();
  }
   }

这是一个创建新对象的 main,将对象的状态打印到控制台,将其序列化 (xml) 到一个文件,从该文件重构一个对象并再次打印对象的状态到控制台。我期望的是一个与序列化内容相匹配的对象。我得到的是将序列化列表的内容添加到默认值的默认对象。

  static void Main(string[] args)
  {
     // Create a default object
     TestConfiguration configuration = new TestConfiguration();
     Console.WriteLine(configuration.ToString());

     // Serialize the object
     XmlSerializer writer = new XmlSerializer(typeof(TestConfiguration));
     StreamWriter filewriter = new StreamWriter("TestConfiguration.xml");
     writer.Serialize(filewriter, configuration);
     filewriter.Close();

     // Now deserialize the xml into another object
     XmlSerializer reader = new XmlSerializer(typeof(TestConfiguration));
     StreamReader filereader = new StreamReader("TestConfiguration.xml");
     TestConfiguration deserializedconfiguration = (TestConfiguration)reader.Deserialize(filereader);
     filereader.Close();

     Console.WriteLine(deserializedconfiguration.ToString());

     Console.ReadLine();
      }

结果:

Name: Pete Sebeck
Associates:
        Jon
        Natalie

Name: Pete Sebeck
Associates:
        Jon
        Natalie
        Jon
        Natalie

我想我一直认为列表 属性 会被设置而不是附加到。有没有人知道集合的反序列化过程?我显然现在知道正确的搜索词,因为我的尝试都是空的。我看到其他帖子描述了我所看到的以及他们自己实现序列化的方法。我更想寻找一个指针来描述当集合被反序列化时会发生什么,这样我就可以向自己解释我所看到的。

你是对的,许多序列化器(虽然不是全部)都是这样工作的。 Json.NET 确实如此,它的 JsonConverter.ReadJson 方法实际上有一个 Object existingValue 正是针对这种情况。

我不知道有任何文档详细说明了这些实现细节。确定序列化程序是否在存在时使用预分配集合而不是无条件分配然后自行设置的最简单方法是使用 ObservableCollection<T> 实际测试它并在更改时附加调试侦听器:

[Serializable]
[DataContract]
public class TestConfiguration
{
    [DataMember]
    public String Name { get { return mName; } set { mName = value; } }

    private String mName = "Pete Sebeck";

    [DataMember]
    public ObservableCollection<String> Associates
    {
        get
        {
            Debug.WriteLine(mAssociates == null ? "Associates gotten, null value" : "Associates gotten, count = " + mAssociates.Count.ToString());
            return mAssociates;
        }
        set
        {
            Debug.WriteLine(value == null ? "Associates set to a null value" : "Associates set, count = " + value.Count.ToString());
            RemoveListeners(mAssociates);
            mAssociates = AddListeners(value);
        }
    }

    private ObservableCollection<String> mAssociates = AddListeners(new ObservableCollection<string>() { "Jon", "Natalie" });

    public override String ToString()
    {
        StringBuilder buffer = new StringBuilder();
        buffer.AppendLine(String.Format("Name: {0}", Name));
        buffer.AppendLine("Associates:");
        foreach (String associate in mAssociates)
        {
            buffer.AppendLine(String.Format("\t{0}", associate));
        }
        return buffer.ToString();
    }

    static ObservableCollection<String> AddListeners(ObservableCollection<String> list)
    {
        if (list != null)
        {
            list.CollectionChanged -= list_CollectionChanged; // In case it was already there.
            list.CollectionChanged += list_CollectionChanged;
        }
        return list;
    }

    static ObservableCollection<String> RemoveListeners(ObservableCollection<String> list)
    {
        if (list != null)
        {
            list.CollectionChanged -= list_CollectionChanged; // In case it was already there.
        }
        return list;
    }

    public static ValueWrapper<bool> ShowDebugInformation = new ValueWrapper<bool>(false);

    static void list_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (!ShowDebugInformation)
            return;
        switch (e.Action)
        {
            case NotifyCollectionChangedAction.Add:
                Debug.WriteLine(string.Format("Added {0} items", e.NewItems.Count));
                break;
            case NotifyCollectionChangedAction.Move:
                Debug.WriteLine("Moved items");
                break;
            case NotifyCollectionChangedAction.Remove:
                Debug.WriteLine(string.Format("Removed {0} items", e.OldItems.Count));
                break;
            case NotifyCollectionChangedAction.Replace:
                Debug.WriteLine("Replaced items");
                break;
            case NotifyCollectionChangedAction.Reset:
                Debug.WriteLine("Reset collection");
                break;
        }
    }
}

public static class TestTestConfiguration
{
    public static void Test()
    {
        var test = new TestConfiguration();

        Debug.WriteLine("\nTesting Xmlserializer...");
        var xml = XmlSerializationHelper.GetXml(test);
        using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
        {
            var testFromXml = XmlSerializationHelper.LoadFromXML<TestConfiguration>(xml);
            Debug.WriteLine("XmlSerializer result: " + testFromXml.ToString());
        }

        Debug.WriteLine("\nTesting Json.NET...");
        var json = JsonConvert.SerializeObject(test, Formatting.Indented);
        using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
        {
            var testFromJson = JsonConvert.DeserializeObject<TestConfiguration>(json);
            Debug.WriteLine("Json.NET result: " + testFromJson.ToString());
        }

        Debug.WriteLine("\nTesting DataContractSerializer...");
        var contractXml = DataContractSerializerHelper.GetXml(test);
        using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
        {
            var testFromContractXml = DataContractSerializerHelper.LoadFromXML<TestConfiguration>(contractXml);
            Debug.WriteLine("DataContractSerializer result: " + testFromContractXml.ToString());
        }

        Debug.WriteLine("\nTesting BinaryFormatter...");
        var binary = BinaryFormatterHelper.ToBase64String(test);
        using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
        {
            var testFromBinary = BinaryFormatterHelper.FromBase64String<TestConfiguration>(binary);
            Debug.WriteLine("BinaryFormatter result: " + testFromBinary.ToString());
        }

        Debug.WriteLine("\nTesting JavaScriptSerializer...");
        var javaScript = new JavaScriptSerializer().Serialize(test);
        using (new SetValue<bool>(TestConfiguration.ShowDebugInformation, true))
        {
            var testFromJavaScript = new JavaScriptSerializer().Deserialize<TestConfiguration>(javaScript);
            Debug.WriteLine("JavaScriptSerializer result: " + testFromJavaScript.ToString());
        }
    }
}

我运行上面的测试,发现:

  1. XmlSerializer 和 Json.NET 使用预先存在的集合(如果存在)。 (在 Json.NET 这可以通过设置 JsonSerializerSettings.ObjectCreationHandling to Replace 来控制)
  2. JavaScriptSerializerBinaryFormatterDataContractSerializer 不会,并且总是自己分配集合。对于后两者,这并不奇怪,因为 do not call default constructors 和直接分配空内存。

我不知道案例 1 中的序列化器为什么会这样。也许他们的作者担心包含的 class 可能想在内部使用被反序列化的集合的 subclass,或者像我一样将观察者附加到可观察的集合完成,所以决定尊重该设计?

请注意 - 对于所有序列化器(可能 BinaryFormatter 除外,对此我不确定),如果集合 属性 被明确声明为数组 那么序列化程序将自己分配数组并在数组完全填充后设置数组。这意味着 arrays can always be used as proxy collections during serialization

通过使用代理数组,您可以确保运行您的集合在反序列化期间被覆盖:

    [IgnoreDataMember]
    [XmlIgnore]
    [ScriptIgnore]
    public ObservableCollection<String> { get; set; } // Or List<string> or etc.

    [XmlArray("Associates")]
    [DataMember(Name="Associates")]
    public string[] AssociateArray
    {
        get
        {
            return (Associates == null ? null : Associates.ToArray());
        }
        set
        {
            if (Associates == null)
                Associates = new ObservableCollection<string>();
            Associates.Clear();
            if (value != null)
                foreach (var item in value)
                    Associates.Add(item);
        }
    }

现在集合返回时只包含之前序列化的成员以及所有 5 个序列化程序。