对自动创建对象的引用

本文关键字:引用 创建对象 | 更新日期: 2023-09-27 17:53:42

我正在尝试序列化和反序列化复杂的对象图:

A 包含一个只读属性,其中包含类型为 B 的对象的不可变数组。类型 B 的对象以及不可变数组在类型 A 的构造函数中创建。

其他类型包含对 B 类型的对象的引用,这些对象是通过访问类型 A 的对象的数组获得的

在反序列化期间,我需要对B的任何引用才能最终指向由 A 构造函数按索引创建的相应对象,而不是从 JSON 创建全新的B对象。我正在尝试将PreserveReferencesHandling与 JSON.NET 一起使用。可以理解的是,这不起作用,因为它尝试使用B的反序列化版本,而不是A构造的版本。

有没有另一种策略可以在这里使用而不修改我的类型?

编辑:为了澄清和非常清楚,解决方案不得修改类型本身。您可以触摸协定解析器、活页夹、引用解析器等,但不能触摸类型。此外,B类型无法反序列化。它们必须由A的构造函数制作。

对自动创建对象的引用

更新

你的

问题没有给出你想要完成什么的例子,所以我猜测你的一些设计要求。 要确认,您的情况是:

  1. 您有一些复杂的对象图可以使用 Json.NET 进行序列化
  2. 在整个图中,有许多类A的实例。
  3. A 包含一个不可变的类 B 实例数组,这些实例只能在 A 的构造函数中构造
  4. A的每个实例可能有也可能没有要序列化的属性(未指定(
  5. B的每个实例可能具有要序列化的属性(未指定(,也可能没有。
  6. 在整个图中也有许多对B实例的引用,但在所有情况下,这些引用实际上都指向A实例之一中的B实例
  7. 反序列化图形时,需要对 B 实例的所有引用,以通过数组索引指向与原始实例对应的A实例中的B实例。
  8. 您没有任何代码来收集和发现对象图中A的所有实例。
  9. 不能以任何方式接触类的 c# 代码,甚至不能添加数据协定属性或私有属性。

让我们使用以下类对这种情况进行建模:

public abstract class B
{
    public int Index { get; set; } // Some property that could be modified.
}
public class A
{
    public class BActual : B
    {
    }
    static int nextId = -1;
    readonly B[] items; // A private read-only array that is never changed.
    public A()
    {
        items = Enumerable.Range(101 + 10 * Interlocked.Increment(ref nextId), 2).Select(i => new BActual { Index = i }).ToArray();
    }
    public string SomeProperty { get; set; }
    public IEnumerable<B> Items
    {
        get
        {
            foreach (var b in items)
                yield return b;
        }
    }
    public string SomeOtherProperty { get; set; }
}
public class MidClass
{
    public MidClass()
    {
        AnotherA = new A();
    }
    public A AnotherA { get; set; }
}
public class MainClass
{
    public MainClass()
    {
        A1 = new A();
        MidClass = new MidClass();
        A2 = new A();
    }
    public List<B> ListOfB { get; set; }
    public A A2 { get; set; }
    public MidClass MidClass { get; set; }
    public A A1 { get; set; }
}

然后,若要序列化,需要使用 Json.NET 在对象图中收集A的所有实例。 接下来,在设置PreserveReferencesHandling = PreserveReferencesHandling.Objects的情况下,将包含所有 A 实例的表序列化为第一项,然后将根对象序列化为第二项。

若要反序列化,使用 PreserveReferencesHandling.Objects 必须使用 JsonConverter for A 反序列化代理类,该反序列化 AB 的属性(如果有(,并添加对序列化"$ref"引用的引用,以B A构造函数中分配的新实例B

因此:

// Used to enable Json.NET to traverse an object hierarchy without actually writing any data.
public class NullJsonWriter : JsonWriter
{
    public NullJsonWriter()
        : base()
    {
    }
    public override void Flush()
    {
        // Do nothing.
    }
}
public class TypeInstanceCollector<T> : JsonConverter where T : class
{
    readonly List<T> instanceList = new List<T>();
    readonly HashSet<T> instances = new HashSet<T>();
    public List<T> InstanceList { get { return instanceList; } }
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }
    public override bool CanRead { get { return false; } }
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        T instance = (T)value;
        if (!instances.Contains(instance))
        {
            instanceList.Add(instance);
            instances.Add(instance);
        }
        // It's necessary to write SOMETHING here.  Null suffices.
        writer.WriteNull();
    }
}
public class ADeserializer : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(A).IsAssignableFrom(objectType);
    }
    public override bool CanWrite { get { return false; } }
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = JObject.Load(reader);
        if (obj == null)
            return existingValue;
        A a;
        var refId = (string)obj["$ref"];
        if (refId != null)
        {
            a = (A)serializer.ReferenceResolver.ResolveReference(serializer, refId);
            if (a != null)
                return a;
        }
        a = ((A)existingValue) ?? new A();
        var items = obj["Items"];
        obj.Remove("Items");
        // Populate properties other than the items, if any
        // This also updates the ReferenceResolver table.
        using (var objReader = obj.CreateReader())
            serializer.Populate(objReader, a);
        // Populate properties of the B items, if any
        if (items != null)
        {
            if (items.Type != JTokenType.Array)
                throw new JsonSerializationException("Items were not an array");
            var itemsArray = (JArray)items;
            if (a.Items.Count() < itemsArray.Count)
                throw new JsonSerializationException("too few items constructucted"); // Item counts must match
            foreach (var pair in a.Items.Zip(itemsArray, (b, o) => new { ItemB = b, JObj = o }))
            {
#if false
                // If your B class has NO properties to deserialize, do this
                var id = (string)pair.JObj["$id"];
                if (id != null)
                    serializer.ReferenceResolver.AddReference(serializer, id, pair.ItemB);
#else
                // If your B class HAS SOME properties to deserialize, do this
                using (var objReader = pair.JObj.CreateReader())
                {
                    // Again, Populate also updates the ReferenceResolver table
                    serializer.Populate(objReader, pair.ItemB);
                }
#endif
            }
        }
        return a;
    }
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}
public class RootProxy<TRoot, TTableItem>
{
    [JsonProperty("table", Order = 1)]
    public List<TTableItem> Table { get; set; }
    [JsonProperty("data", Order = 2)]
    public TRoot Data { get; set; }
}
public class TestClass
{
    public static string Serialize(MainClass main)
    {
        // First, collect all instances of A 
        var collector = new TypeInstanceCollector<A>();
        var collectionSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, Converters = new JsonConverter[] { collector } };
        using (var jsonWriter = new NullJsonWriter())
        {
            JsonSerializer.CreateDefault(collectionSettings).Serialize(jsonWriter, main);
        }
        // Now serialize a proxt class with the collected instances of A at the beginning, to establish reference ids for all instances of B.
        var proxy = new RootProxy<MainClass, A> { Data = main, Table = collector.InstanceList };
        var serializationSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects };
        return JsonConvert.SerializeObject(proxy, Formatting.Indented, serializationSettings);
    }
    public static MainClass Deserialize(string json)
    {
        var serializationSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, Converters = new JsonConverter[] { new ADeserializer() } };
        var proxy = JsonConvert.DeserializeObject<RootProxy<MainClass, A>>(json, serializationSettings);
        return proxy.Data;
    }
    static IEnumerable<A> GetAllA(MainClass main)
    {
        // For testing.  In your case apparently you can't do this manually.
        if (main.A1 != null)
            yield return main.A1;
        if (main.A2 != null)
            yield return main.A2;
        if (main.MidClass != null && main.MidClass.AnotherA != null)
            yield return main.MidClass.AnotherA;
    }
    static IEnumerable<B> GetAllB(MainClass main)
    {
        return GetAllA(main).SelectMany(a => a.Items);
    }
    public static void Test()
    {
        var main = new MainClass();
        main.A1.SomeProperty = "main.A1.SomeProperty";
        main.A1.SomeOtherProperty = "main.A1.SomeOtherProperty";
        main.A2.SomeProperty = "main.A2.SomeProperty";
        main.A2.SomeOtherProperty = "main.A2.SomeOtherProperty";
        main.MidClass.AnotherA.SomeProperty = "main.MidClass.AnotherA.SomeProperty";
        main.MidClass.AnotherA.SomeOtherProperty = "main.MidClass.AnotherA.SomeOtherProperty";
        main.ListOfB = GetAllB(main).Reverse().ToList();
        var json = Serialize(main);
        var main2 = Deserialize(json);
        var json2 = Serialize(main2);
        foreach (var b in main2.ListOfB)
            Debug.Assert(GetAllB(main2).Contains(b)); // No assert
        Debug.Assert(json == json2); // No assert
        Debug.Assert(main.ListOfB.Select(b => b.Index).SequenceEqual(main2.ListOfB.Select(b => b.Index))); // No assert
        Debug.Assert(GetAllA(main).Select(a => a.SomeProperty + a.SomeOtherProperty).SequenceEqual(GetAllA(main2).Select(a => a.SomeProperty + a.SomeOtherProperty))); // No assert
    }
}

原始答案

首先,可以使用 [JsonConstructor] 属性指定 Json.NET 应使用非默认构造函数来反序列化类A。 这样做将允许您反序列化到不可变集合中。 此构造函数可以是私有的,以便您可以继续在预先存在的公共构造函数中创建B实例。 请注意,构造函数参数名称必须与原始属性名称匹配。

其次,如果设置了 PreserveReferencesHandling = PreserveReferencesHandling.Objects ,则对象图中直接引用不可变数组所持有B实例的任何其他对象在序列化和反序列化时将继续直接引用反序列化不可变数组中的实例。 也就是说,它应该只是工作。

请考虑以下测试用例:

public class B
{
    public int Index { get; set; }
}
public class A
{
    static int nextId = -1;
    readonly B [] items; // A private read-only array that is never changed.
    [JsonConstructor]
    private A(IEnumerable<B> Items, string SomeProperty)
    {
        this.items = (Items ?? Enumerable.Empty<B>()).ToArray();
        this.SomeProperty = SomeProperty;
    }
    // // Create instances of "B" with different properties each time the default constructor is called.
    public A() : this(Enumerable.Range(101 + 10*Interlocked.Increment(ref nextId), 2).Select(i => new B { Index = i }), "foobar") 
    {
    }
    public IEnumerable<B> Items
    {
        get
        {
            foreach (var b in items)
                yield return b;
        }
    }
    [JsonIgnore]
    public int Count { get { return items.Length; } }
    public B GetItem(int index)
    {
        return items[index];
    }
    public string SomeProperty { get; set; }
    public string SomeOtherProperty { get; set; }
}
public class TestClass
{
    public A A { get; set; }
    public List<B> ListOfB { get; set; }
    public static void Test()
    {
        var a = new A() { SomeOtherProperty = "something else" };
        var test = new TestClass { A = a, ListOfB = a.Items.Reverse().ToList() };
        var settings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects };
        var json = JsonConvert.SerializeObject(test, Formatting.Indented, settings);
        Debug.WriteLine(json);
        var test2 = JsonConvert.DeserializeObject<TestClass>(json, settings);
        // Assert that pointers in "ListOfB" are equal to pointers in A.Items
        Debug.Assert(test2.ListOfB.All(i2 => test2.A.Items.Contains(i2, new ReferenceEqualityComparer<B>())));
        // Assert deserialized data is the same as the original data.
        Debug.Assert(test2.A.SomeProperty == test.A.SomeProperty);
        Debug.Assert(test2.A.SomeOtherProperty == test.A.SomeOtherProperty);
        Debug.Assert(test2.A.Items.Select(i => i.Index).SequenceEqual(test.A.Items.Select(i => i.Index)));
        var json2 = JsonConvert.SerializeObject(test2, Formatting.Indented, settings);
        Debug.WriteLine(json2);
        Debug.Assert(json2 == json);
    }
}

在这种情况下,我创建了包含一些数据的类B,类A,其中包含在其公共构造函数中创建的不可变B集合,以及一个包含A实例和BA获取的项目列表的包含类TestClass。 当我序列化它时,我得到以下 JSON:

{
  "$id": "1",
  "A": {
    "$id": "2",
    "Items": [
      {
        "$id": "3",
        "Index": 101
      },
      {
        "$id": "4",
        "Index": 102
      }
    ],
    "SomeProperty": "foobar",
    "SomeOtherProperty": "something else"
  },
  "ListOfB": [
    {
      "$ref": "4"
    },
    {
      "$ref": "3"
    }
  ]
}
然后,当我反序列化

它时,我断言ListOfB B的所有反序列化项都与 a.Items 中的 B 实例之一具有指针相等。 我还断言所有反序列化属性都具有与原始属性相同的值,从而确认调用非默认私有构造函数来反序列化不可变集合。

这是你想要的吗?

为了检查B实例的指针相等性,我使用:

public class ReferenceEqualityComparer<T> : IEqualityComparer<T> where T : class
{
    #region IEqualityComparer<T> Members
    public bool Equals(T x, T y)
    {
        return object.ReferenceEquals(x, y);
    }
    public int GetHashCode(T obj)
    {
        return (obj == null ? 0 : obj.GetHashCode());
    }
    #endregion
}