为什么JsonConvert区别对待默认构造函数和参数化构造函数



我正在尝试Json.net序列化和反序列化字典的能力,多亏了这篇文章,我能够找到一个很好的解决方案,以简单的方式序列化字典。

它工作得很好,但在某种情况下,它以一种(对我来说)荒谬的方式崩溃了,以至于我忍不住花了接下来的三个小时进行调试。

问题来了。序列化此类时

public class ReferenceTesting
{
public List<Scenario> scenarios = new List<Scenario>();
private Dictionary<Scenario, float> _Dict = new Dictionary<Scenario, float>();
[JsonProperty]
public List<KeyValuePair<Scenario, float>> SerializedDict
{
get { return _Dict.ToList(); }
set { _Dict = value.ToDictionary(x => x.Key, x => x.Value); }
}
public ReferenceTesting(int number = 0)
{
for (int i = 0; i < number; i++)
{
Scenario s1 = new Scenario();
scenarios.Add(s1);
_Dict.Add(s1, i);
}
}
public override string ToString()
{
string s = "";
for (int i = 0; i < scenarios.Count(); i++)
{
Scenario scenario = scenarios[i];
s += $"scenario{i} n";
}
foreach (KeyValuePair<Scenario, float> scenario in SerializedDict)
{
s += $"Key: {scenario.Key}, Value: {scenario.Value} n";
}
return s;
}
}

一切都按预期进行,这意味着当我实例化时

new Reference(3);

然后序列化和反序列化,我最终得到了一个对象,该对象在列表中有3项,在字典中有3个项。

输出:

scenario0 
scenario1 
scenario2 
Key: Scenario, Value: 0 
Key: Scenario, Value: 1 
Key: Scenario, Value: 2 

但是,通过添加默认构造函数

public ReferenceTesting() { }

序列化可以工作,在列表和字典中写出3个项,但反序列化不适用于该属性。这意味着我最终会使用

scenario0 
scenario1 
scenario2 

作为输出。

最大的惊喜是,这两个构造函数做的事情完全相同——当数字=0时,什么都没有(Json.net创建它时,我仔细检查了一下)。因此,这意味着,如果有或没有默认构造函数,序列化程序必须在后台做一些事情来区别对待属性。

Json.NET如何在具有参数化构造函数的对象与具有默认构造函数的对象之间以不同的方式反序列化对象

Json.NET是一个流式反序列化程序。只要可能,它就会在JSON中进行反序列化,而不是在最终反序列化之前将完整的JSON预加载到中间表示中。

因此,当使用默认构造函数反序列化JSON对象时,它首先构造相应的.Net对象。然后,它通过JSON中的键/值对进行流式传输,递归地填充对象的.Net成员,直到JSON对象结束。对于遇到的每一对,它都会找到相应的.Net成员。如果值是基元类型,它将反序列化基元并设置值。但是,如果值是复杂类型(JSON对象或数组),它会在必要时构造子对象,将值设置回父对象中,然后在继续流式传输时递归填充。

但是,当使用参数化构造函数反序列化对象时,Json.NET不能使用此流算法,而必须首先将Json对象完全反序列化为反序列化的.NET名称/值对的中间表,按名称将每个JSON值与其对应的.Net构造函数参数或属性匹配,然后反序列化为.Net中声明的类型。只有这样,才能通过将反序列化的构造函数参数传递到构造函数中,并将其余部分设置为属性值来构造对象。

有关此过程的详细信息,请参阅

  • JSON.net:如何在不使用默认构造函数的情况下进行反序列化
  • C#中的JSON反序列化是如何工作的

(对于ISerializable对象,有第三种算法不适用于您的情况。)

通过默认构造函数反序列化时,为什么我的代理public List<KeyValuePair<Scenario, float>> SerializedDict属性没有正确反序列化

原因在的回答中解释了为什么当用.NET Newtonsoft.json组件反序列化一些有效的json时,我的POCO中的所有集合都为null,这是关于json.NET的Populate()算法的细节:

  1. 它调用父类中的getter来获取正在反序列化的属性的当前值。

  2. 如果为null,并且除非使用自定义构造函数,否则它会分配属性返回类型的实例(使用类型的JsonContract.DefaultCreator方法)。

  3. 它调用父级中的setter,将分配的实例设置回父级中。

  4. 它继续填充类型的实例。

  5. 在填充实例之后,它不会再次将实例设置回原处

因此,在填充列表后不会调用SerializedDict的setter。

但是,当父类具有参数化构造函数时,属性值SerializedDict在构造其父类之前会被完全反序列化,因此使用完全填充的代理列表来调用setter。

如何创建在这两种情况下都有效的代理集合属性

您可以使用数组而不是列表。由于数组无法调整大小,因此必须对其进行完全反序列化和填充,然后才能将其设置回父对象中:

public class ReferenceTesting
{
public KeyValuePair<Scenario, float> [] SerializedDict
{
get { return _Dict.ToArray(); }
set { _Dict = value.ToDictionary(x => x.Key, x => x.Value);  }
}
// Remainder unchanged

如果需要,可以将数组属性标记为[JsonProperty],使其成为private

顺便说一下,您当前的反序列化在scenarios_Dict集合中创建了重复的Scenario对象,如这里的演示fiddle#1所示。

解决此问题的一种方法是仅序列化_Dict(假设所有场景都在字典中)。另一种是使用PreserveReferencesHandling,例如通过将[JsonObject(IsReference = true)]添加到Scenario:

[JsonObject(IsReference = true)]
public class Scenario
{
// Remainder unchanged
}

注:

  • JSON中没有引用序列化的标准。Json.NET的实现可能与其他序列化程序的实现不匹配。

  • PreserveReferencesHandling不适用于具有参数化构造函数的对象(有关详细信息,请参阅此处),因此请确保Scenario没有参数化构造函数。

这里的演示fiddle#2显示了使用默认构造函数正常工作的一切,而这里的演示fiddle#3显示了使用参数化构造函数正常工作。

最新更新