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

Rob*_*win 5 c# xml serialization xmlserializer

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

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

[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();
  }
   }
Run Code Online (Sandbox Code Playgroud)

这里的 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();
      }
Run Code Online (Sandbox Code Playgroud)

结果:

Name: Pete Sebeck
Associates:
        Jon
        Natalie

Name: Pete Sebeck
Associates:
        Jon
        Natalie
        Jon
        Natalie
Run Code Online (Sandbox Code Playgroud)

我想我一直认为 List 属性将被设置而不是附加。有谁知道集合的反序列化过程吗?我显然现在知道正确的搜索词,因为我的尝试都是空的。我看到其他帖子描述了我所看到的以及他们自己实现序列化的方法。我更多地寻找一个指针来描述集合反序列化时发生的情况,以便我可以向自己解释我所看到的内容。

dbc*_*dbc 4

您是对的,许多序列化器(尽管不是全部)都以这种方式工作。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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我进行了上面的测试,发现:

  1. XmlSerializerJson.NET 使用预先存在的集合(如果存在)。(在 Json.NET 中,可以通过设置JsonSerializerSettings.ObjectCreationHandling来控制Replace
  2. JavaScriptSerializerBinaryFormatter并且DataContractSerializer不这样做,并且始终自己分配集合。对于后两者,这并不奇怪,因为两者都不调用默认构造函数,而是直接分配空内存。

我不知道为什么情况 1 中的序列化器会这样。也许他们的作者担心包含类可能想要在内部使用正在反序列化的集合的子类,或者像我所做的那样将观察者附加到可观察的集合,因此决定尊重该设计?

需要注意的是,对于所有序列化器(也许除了,BinaryFormatter我不确定),如果将集合属性专门声明为数组,那么序列化器将分配数组本身并在完全填充后设置数组。这意味着数组在序列化期间始终可以用作代理集合

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

    [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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

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