Explanation for ObjectCreationHandling using Newtonsoft JSON

本文关键字:Newtonsoft JSON using ObjectCreationHandling for Explanation | 更新日期: 2023-09-27 18:36:59

我正在追踪一个错误,我注意到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<>。这是预期行为吗?原因是什么?

Explanation for ObjectCreationHandling using Newtonsoft JSON

它基本上归结为类型实例化和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返回falseIsValueType()返回false - 因此重用基础现有值。

对于ArrayIsValueType()也是false的,但是由于明显的原因true IsReadOnlyOrFixedSize,因此useExistingValue标志设置为false,并且SetPropertyValue方法中的CreateValueInternal调用接收null引用,该引用指示不是重用现有值,而是创建一个新值, 然后在新实例上设置。

如前所述,可以使用 ObjectCreationHandling.Replace 更改此行为,因为在 CalculatePropertyDetails 方法中设置useExistingValue之前会检查此行为。