问题描述
对于应用程序配置,我经常会创建一个配置类,其中包含应用程序的配置值,然后我将其反序列化为要使用的对象.配置对象通常是绑定到用户界面控件的数据,以便用户可以更改和保存配置.配置类通常具有分配给属性的默认值,以便始终有一个默认配置.这很有效.我最近遇到了一个情况,我有一个字符串列表,提供了一些默认路径信息.我所看到的让我意识到我并不完全知道在 XML 反序列化到对象期间对象属性是如何填充的.
所以我创建了一个简单的例子来展示这种行为.下面是一个简单的类,它有几个具有一些代码默认值的属性.
[可序列化]公共类测试配置{公共字符串名称{得到{返回名称;}放{mName = 值;}}private String mName = "Pete Sebeck";公共列表合伙人{得到{返回 massociates;}放{mAssociates = 值;}} 私人列表mAssociates = new List() { "Jon", "Natalie" };公共覆盖字符串 ToString(){StringBuilder 缓冲区 = new StringBuilder();buffer.AppendLine(String.Format("Name: {0}", Name));buffer.AppendLine("同事:");foreach(mAssociates 中的字符串关联){buffer.AppendLine(String.Format(" {0}", associate));}返回缓冲区.ToString();}}
这里是一个 main,它创建一个新对象,将对象的状态打印到控制台,将它序列化 (xml) 到一个文件,从该文件中重新构建一个对象,然后再次将对象的状态打印到安慰.我期望的是一个与序列化内容相匹配的对象.我得到的是默认对象,其中序列化列表的内容添加到默认值中.
static void Main(string[] args){//创建一个默认对象TestConfiguration 配置 = new TestConfiguration();Console.WriteLine(configuration.ToString());//序列化对象XmlSerializer writer = new XmlSerializer(typeof(TestConfiguration));StreamWriter filewriter = new StreamWriter("TestConfiguration.xml");writer.Serialize(filewriter, 配置);filewriter.Close();//现在将 xml 反序列化为另一个对象XmlSerializer reader = new XmlSerializer(typeof(TestConfiguration));StreamReader filereader = new StreamReader("TestConfiguration.xml");TestConfiguration deserializedconfiguration = (TestConfiguration)reader.Deserialize(filereader);文件阅读器.关闭();Console.WriteLine(deserializedconfiguration.ToString());Console.ReadLine();}
结果:
姓名:Pete Sebeck同事:乔恩娜塔莉姓名:皮特·塞贝克同事:乔恩娜塔莉乔恩娜塔莉
我想我一直认为 List 属性会被设置而不是附加到.有没有人有指向集合的反序列化过程的指针?我现在显然知道正确的搜索词,因为我的尝试是空的.我看到其他帖子描述了我所看到的内容以及他们自己实现序列化的方法.我更多的是在寻找一个指针来描述反序列化集合时会发生什么,以便我可以向自己解释我所看到的.
您是对的,许多序列化程序(尽管不是全部)以这种方式工作.Json.NET 确实如此,它的 JsonConverter.ReadJson
方法实际上有一个 Object existingValue
用于这种情况.
我不知道有任何文件详细说明了这些类型的实现细节.确定序列化程序是否在存在时使用预先分配的集合而不是无条件分配然后自行设置集合的最简单方法是通过使用 ObservableCollection
实际测试它并在它出现时附加调试侦听器改变了:
[可序列化][数据合约]公共类测试配置{[数据成员]公共字符串名称 { 获取 { 返回 mName;} set { mName = value;} }private String mName = "Pete Sebeck";[数据成员]公共 ObservableCollection合伙人{得到{Debug.WriteLine(mAssociates == null ? "Associates got, null value" : "Associates got, count = " + mAssociates.Count.ToString());返回 massociates;}放{Debug.WriteLine(value == null ? "Associates set to a null value" : "Associates set, count = " + value.Count.ToString());删除监听器(mAssociates);mAssociates = AddListeners(value);}}私有 ObservableCollectionmAssociates = AddListeners(new ObservableCollection() { "Jon", "Natalie" });公共覆盖字符串 ToString(){StringBuilder 缓冲区 = new StringBuilder();buffer.AppendLine(String.Format("Name: {0}", Name));buffer.AppendLine("同事:");foreach(mAssociates 中的字符串关联){buffer.AppendLine(String.Format(" {0}", associate));}返回缓冲区.ToString();}静态 ObservableCollectionAddListeners(ObservableCollection 列表){如果(列表!= null){list.CollectionChanged -= list_CollectionChanged;//如果它已经存在.list.CollectionChanged += list_CollectionChanged;}退货清单;}静态 ObservableCollectionRemoveListeners(ObservableCollection 列表){如果(列表!= null){list.CollectionChanged -= list_CollectionChanged;//如果它已经存在.}退货清单;}公共静态 ValueWrapperShowDebugInformation = new ValueWrapper(false);static void list_CollectionChanged(对象发送者,NotifyCollectionChangedEventArgs e){如果 (!ShowDebugInformation)返回;开关(e.Action){案例 NotifyCollectionChangedAction.Add:Debug.WriteLine(string.Format("添加了 {0} 个项目", e.NewItems.Count));休息;案例 NotifyCollectionChangedAction.Move:Debug.WriteLine("移动的项目");休息;案例 NotifyCollectionChangedAction.Remove:Debug.WriteLine(string.Format("Removed {0} items", e.OldItems.Count));休息;案例 NotifyCollectionChangedAction.Replace:Debug.WriteLine("替换项目");休息;案例 NotifyCollectionChangedAction.Reset:Debug.WriteLine("重置集合");休息;}}}公共静态类 TestTestConfiguration{公共静态无效测试(){var test = new TestConfiguration();Debug.WriteLine("
测试 Xmlserializer...");var xml = XmlSerializationHelper.GetXml(test);使用 (new SetValue(TestConfiguration.ShowDebugInformation, true)){var testFromXml = XmlSerializationHelper.LoadFromXML(xml);Debug.WriteLine("XmlSerializer 结果:" + testFromXml.ToString());}Debug.WriteLine("
测试 Json.NET...");var json = JsonConvert.SerializeObject(test, Formatting.Indented);使用 (new SetValue(TestConfiguration.ShowDebugInformation, true)){var testFromJson = JsonConvert.DeserializeObject<TestConfiguration>(json);Debug.WriteLine("Json.NET 结果:" + testFromJson.ToString());}Debug.WriteLine("
正在测试 DataContractSerializer...");var contractXml = DataContractSerializerHelper.GetXml(test);使用 (new SetValue(TestConfiguration.ShowDebugInformation, true)){var testFromContractXml = DataContractSerializerHelper.LoadFromXML(contractXml);Debug.WriteLine("DataContractSerializer 结果:" + testFromContractXml.ToString());}Debug.WriteLine("
正在测试 BinaryFormatter...");var binary = BinaryFormatterHelper.ToBase64String(test);使用 (new SetValue(TestConfiguration.ShowDebugInformation, true)){var testFromBinary = BinaryFormatterHelper.FromBase64String(binary);Debug.WriteLine("BinaryFormatter 结果:" + testFromBinary.ToString());}Debug.WriteLine("
测试 JavaScriptSerializer...");var javaScript = new JavaScriptSerializer().Serialize(test);使用 (new SetValue(TestConfiguration.ShowDebugInformation, true)){var testFromJavaScript = new JavaScriptSerializer().Deserialize(javaScript);Debug.WriteLine("JavaScriptSerializer 结果:" + testFromJavaScript.ToString());}}}
我进行了上面的测试,发现:
XmlSerializer
和 Json.NET 使用预先存在的集合(如果存在).(在 Json.NET 中,这可以通过设置JsonSerializerSettings.ObjectCreationHandling代码>
到替换
)JavaScriptSerializer
、BinaryFormatter
和DataContractSerializer
不会,并且总是自己分配集合.对于后两者,这并不奇怪,因为不调用默认构造函数,而是简单地直接分配空内存.
我不知道为什么情况 1 中的序列化程序会这样.也许他们的作者担心包含类可能想要在内部使用被反序列化的集合的子类,或者像我所做的那样将观察者附加到可观察的集合,因此决定尊重这种设计?>
注意 - 对于所有序列化程序(可能除了 BinaryFormatter
,我不确定),如果集合属性 被明确声明为数组,那么序列化程序将分配数组本身并在完全填充后设置数组.这意味着 数组始终可以用作代理序列化期间的集合.
通过使用代理数组,你可以保证你的集合在反序列化过程中被覆盖:
[IgnoreDataMember][XmlIgnore][脚本忽略]公共 ObservableCollection{ 得到;放;}//或者 List或等[XmlArray("关联")][DataMember(Name="Associates")]公共字符串[] AssociateArray{得到{return (Associates == null ? null : Associates.ToArray());}放{如果(关联== null)Associates = new ObservableCollection();Associates.Clear();如果(值!= null)foreach (var item in value)Associates.Add(item);}}
现在该集合仅返回带有所有 5 个序列化程序的先前序列化成员.
For application configuration, I frequently will create a configuration class with configuration values for the application that I then deserialize into an object to utilize. The configuration object is usually databound to a user interface control so that the configuration can be changed and persisted by the user. The configuration class typically has default values assigned to the properties so that there is always a default configuration. This has worked well. I recently had a situation where I had a list of strings that provided some default path information. And what I saw made me realize I did not completely know how the object properties are being populated during XML deserialization to an object.
So I created a simple example to show the behavior. The following is a simple class that has a couple of properties that have some code defaults.
[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(" {0}", associate));
}
return buffer.ToString();
}
}
And here is a main that creates a new objects, prints the state of the object to the console, serializes (xml) it to a file, the reconstitutes an object from that file and again prints the state of the object to the console. What I expected was an object that matched what was serialized. What I got was the default object with contents of the serialized list added to the default.
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();
}
Results:
Name: Pete Sebeck
Associates:
Jon
Natalie
Name: Pete Sebeck
Associates:
Jon
Natalie
Jon
Natalie
I guess I always thought the List property would be set rather than appended to. Does anyone have a pointer to the deserialization process for collections? I apparently do now know the correct search terms as my attempts are coming up empty. I see other posts describing what I am seeing and their approach of implementing serialization themselves. I am more looking for a pointer that describes what happens when a collection is deserialized so I can explain to myself what I am seeing.
You are correct that many serializers (though not all) work this way. Json.NET does, its JsonConverter.ReadJson
method actually has an Object existingValue
for exactly this situation.
I don't know of any documents where these sorts of implementation details are spelled out. The easiest way to determine whether a serializer uses pre-allocated collections when present rather than unconditionally allocating and then setting one itself is to actually test it by using an ObservableCollection<T>
and attaching debug listeners when it is changed:
[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(" {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("
Testing 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("
Testing 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("
Testing 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("
Testing 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("
Testing 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());
}
}
}
I ran the test above, and found:
XmlSerializer
and Json.NET use the pre-existing collection if present. (In Json.NET this can be controlled by settingJsonSerializerSettings.ObjectCreationHandling
toReplace
)JavaScriptSerializer
,BinaryFormatter
andDataContractSerializer
do not, and always allocate the collection themselves. For the latter two this is not surprising as both do not call default constructors and instead simply allocate empty memory directly.
I don't know why the serializers in case 1 behave this way. Perhaps their authors were concerned that the containing class might want to internally use a subclass of the collection being deserialized, or attach observers to observable collections as I have done, and so decided to honor that design?
One note - for all serializers (except, maybe, BinaryFormatter
, about which I am unsure), if a collection property is declared specifically as an array then the serializer will allocate the array itself and set the array after it is fully populated. This means that arrays can always be used as proxy collections during serialization.
By using a proxy array, you can guarantee that your collection is overwritten during deserialization:
[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);
}
}
Now the collection comes back with only the previously serialized members with all 5 serializers.
这篇关于具有代码默认值的集合属性的 XML 反序列化的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!