我正在追踪一个错误,我注意到Newtonsoft JSON会将项目附加到默认构造函数中初始化的List<>
。我做了更多的挖掘,并与 C# 聊天中的一些人进行了讨论,我们注意到此行为不适用于所有其他集合类型。
https://dotnetfiddle.net/ikNyiT
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Collections.ObjectModel;
public class TestClass
{
public Collection<string> Collection = new Collection<string>(new [] { "ABC", "DEF" });
public List<string> List = new List<string>(new [] { "ABC", "DEF" });
public ReadOnlyCollection<string> ReadOnlyCollection = new ReadOnlyCollection<string>(new [] { "ABC", "DEF" });
}
public class Program
{
public static void Main()
{
var serialized = @"{
Collection: [ 'Goodbye', 'AOL' ],
List: [ 'Goodbye', 'AOL' ],
ReadOnlyCollection: [ 'Goodbye', 'AOL' ]
}";
var testObj = JsonConvert.DeserializeObject<TestClass>(serialized);
Console.WriteLine("testObj.Collection: " + string.Join(",", testObj.Collection));
Console.WriteLine("testObj.List: " + string.Join(",", testObj.List));
Console.WriteLine("testObj.ReadOnlyCollection: " + string.Join(",", testObj.ReadOnlyCollection));
}
}
输出:
testObj.Collection: ABC,DEF
testObj.List: ABC,DEF,Goodbye,AOL
testObj.ReadOnlyCollection: Goodbye,AOL
如您所见,Collection<>
属性不受反序列化的影响,List<>
被追加并替换ReadOnlyCollection<>
。这是预期行为吗?原因是什么?
它基本上归结为类型实例化和ObjectCreationHandling
设置。ObjectCreationHandling
有三种设置
自动 0 重用现有对象,在需要时创建新对象。
重用 1 仅重用现有对象。
替换 2 始终创建新对象。
默认值为 auto
(第 44 行)。
只有在一系列检查确定当前类型是否具有 null 的TypeInitializer
后,才会覆盖 Auto。此时,它会检查是否存在无参数构造函数。
///
创建一个工厂函数,该函数可用于创建 JsonConverter 的实例,由
参数类型。
然后,返回的函数可用于调用转换器的默认 ctor,或任何
通过对象数组参数化构造函数。
///
本质上它的行为是这样的(它看起来像 6 个类中大约 1500 行代码)。
ObjectCreationHandling och = ObjectCreationHandling.Auto;
if( typeInitializer == null )
{
if( parameterlessConstructor )
{
och = ObjectCreationHandling.Reuse;
}
else
{
och = ObjectCreationHandling.Replace;
}
}
此设置是 JsonSerializerSettings 的一部分,该设置由 DeserializeObject 的访问者模式构造函数内部组成。如上所示,每个设置都有不同的功能。
回到 List、Collection 和 ReadOnlyCollection,我们将查看每个条件语句的集合。
列表
testObj.List.GetType().TypeInitializer == null
是错误的。因此,List
接收默认的 ObjectCreationHandling.Auto,并在反序列化期间使用 testObj 实例的实例化列表,以及使用 serialized
字符串实例化的新列表。
testObj.List: ABC,DEF,Goodbye,AOL
收集
testObj.Collection.GetType().TypeInitializer == null
为 true,表示没有可用的反射类型初始值设定项,因此我们转到下一个条件,即检查是否存在无参数构造函数。 testObj.Collection.GetType().GetConstructor(Type.EmptyTypes) == null
是错误的。因此,Collection
接收 ObjectCreationHandling.Reuse 的值(仅重用现有对象)。集合的实例化实例从 testObj 使用,但无法实例化serialized
字符串。
testObj.Collection: ABC,DEF
只读集合
testObj.ReadOnlyCollection.GetType().TypeInitializer == null
为 true,表示没有可用的反射类型初始值设定项,因此我们转到下一个条件,即检查是否存在无参数构造函数。 testObj.ReadOnlyCollection.GetType().GetConstructor(Type.EmptyTypes) == null
也是如此。因此,ReadOnlyCollection 接收 ObjectCreationHandling.Replace 的值(始终创建新对象)。仅使用serialized
字符串中的实例化值。
testObj.ReadOnlyCollection: Goodbye,AOL
虽然这个问题已经解决,但我最初想把这个答案发布到一个重复的问题,但这个问题已经关闭了,所以我在这里发布我的答案,因为它包含一些内部观点。
因为 Json.NET 是开源的,所以我们可以幸运地将原因追溯到其根源:-)。
如果您检查 Json.NET 源,则可以找到处理反序列化的类JsonSerializerInternalReader
(此处为完整源代码)。此类有一个方法 SetPropertyValue
,该方法在新创建的对象(代码缩写)上设置反序列化值:
private bool SetPropertyValue(JsonProperty property, ..., object target)
{
...
if (CalculatePropertyDetails(
property,
...,
out useExistingValue,
... ))
{
return false;
}
...
if (propertyConverter != null && propertyConverter.CanRead)
{
...
}
else
{
value = CreateValueInternal(
...,
(useExistingValue) ? currentValue : null);
}
if ((!useExistingValue || value != currentValue)
&& ShouldSetPropertyValue(property, value))
{
property.ValueProvider.SetValue(target, value);
...
return true;
}
return useExistingValue;
}
如您所见,有一个布尔标志useExistingValue
用于确定是重用还是替换现有值。
CalculatePropertyDetails
方法内部是以下代码片段:
if ((objectCreationHandling != ObjectCreationHandling.Replace)
&& (tokenType == JsonToken.StartArray || tokenType == JsonToken.StartObject)
&& property.Readable)
{
currentValue = property.ValueProvider.GetValue(target);
gottenCurrentValue = true;
if (currentValue != null)
{
...
useExistingValue = (
!propertyContract.IsReadOnlyOrFixedSize &&
!propertyContract.UnderlyingType.IsValueType());
}
}
对于基础集合List<T>
,IsReadOnlyOrFixedSize
返回false
,IsValueType()
返回false
- 因此重用基础现有值。
对于Array
,IsValueType()
也是false
的,但是由于明显的原因true
IsReadOnlyOrFixedSize
,因此useExistingValue
标志设置为false
,并且SetPropertyValue
方法中的CreateValueInternal
调用接收null
引用,该引用指示不是重用现有值,而是创建一个新值, 然后在新实例上设置。
如前所述,可以使用 ObjectCreationHandling.Replace
更改此行为,因为在 CalculatePropertyDetails
方法中设置useExistingValue
之前会检查此行为。