【问题标题】:Why is Equals method called before full deserialization of an object?为什么在对象的完全反序列化之前调用 Equals 方法?
【发布时间】:2019-11-25 06:38:15
【问题描述】:

我想序列化和反序列化包含在字典中的对象,实现Equals 方法并在里面包含字典。但是它会导致ArgumentException,因为Equals 方法在反序列化时调用过早。

using System.Collections.Generic;
using System.Runtime.Serialization;
using NUnit.Framework;

[Serializable]
public class SerializableObject
{
    public Dictionary<string, bool> Values = new Dictionary<string, bool>();

    public override bool Equals(object obj)
    {
        if (!(obj is SerializableObject other))
            return false;
        if (ReferenceEquals(this, obj))
            return true;
        if (Values.Count != other.Values.Count)
            return false;
        foreach (var (k, v) in Values)
            if (!other.Values.TryGetValue(k, out var otherV) || v != otherV)
                return false;
        return true;
    }

    public override int GetHashCode()
    {
        return 0;
    }
}

[Test]
public void DeserializeDictionary()
{
    using (var stream = new MemoryStream())
    {
        var obj1 = new SerializableObject();
        obj1.Values["aaa"] = true;
        var obj2 = new SerializableObject();
        obj2.Values["bbb"] = false;

        var formatter = new BinaryFormatter();

        var dict = new Dictionary<SerializableObject, bool>
        {
            [obj1] = true,
            [obj2] = false
        };

        formatter.Serialize(stream, dict);
        stream.Seek(0, SeekOrigin.Begin);
        dict = (Dictionary<SerializableObject, bool>)formatter.Deserialize(stream);

        Assert.Contains(obj1, dict.Keys);
        Assert.Contains(obj2, dict.Keys);
    }
}

反序列化时,对两个具有空字典的对象调用Equals 方法,并发生ArgumentException(“已添加具有相同键的项”)。

另外,如果对象实现了[OnDeserialized]方法,当字典还是空的时候也会调用这个方法,稍后会填充内部字典,然后返回整个顶级字典。

编辑:在这个简短的示例中,GetHashCode 故意返回 0 以强制调用 Equals。在现实世界的程序中,只有两个具有相同哈希码的不相等对象(冲突)。

编辑 2:这是一个嵌套对象的更复杂示例。

[Serializable]
public class SerializableObject : ISerializable
{
    public SerializableObject() {}

    public Dictionary<string, bool> Values = new Dictionary<string, bool>();

    public override bool Equals(object obj)
    {
        if (!(obj is SerializableObject other))
            return false;
        if (ReferenceEquals(this, obj))
            return true;
        if (Values.Count != other.Values.Count)
            return false;
        foreach (var kvp in Values)
            if (!other.Values.TryGetValue(kvp.Key, out var otherV) || kvp.Value != otherV)
                return false;
        return true;
    }

    public override int GetHashCode() => 0;

    public void GetObjectData(SerializationInfo info, StreamingContext context) =>
        info.AddValue(nameof(Values), Values.ToArray());

    private SerializableObject(SerializationInfo info, StreamingContext context) =>
        Values = new Dictionary<string, bool>(info.Get<KeyValuePair<string, bool>[]>(nameof(Values)));
}

[Serializable]
public class SerializableObject2 : ISerializable
{
    public SerializableObject2() { }

    public Dictionary<SerializableObject, bool> Values = new Dictionary<SerializableObject, bool>();

    public override bool Equals(object obj)
    {
        if (!(obj is SerializableObject2 other))
            return false;
        if (ReferenceEquals(this, obj))
            return true;
        if (Values.Count != other.Values.Count)
            return false;
        foreach (var kvp in Values)
            if (!other.Values.TryGetValue(kvp.Key, out var otherV) || kvp.Value != otherV)
                return false;
        return true;
    }

    public override int GetHashCode() => 0;

    public void GetObjectData(SerializationInfo info, StreamingContext context) =>
        info.AddValue(nameof(Values), Values.ToArray());

    private SerializableObject2(SerializationInfo info, StreamingContext context) =>
        Values = new Dictionary<SerializableObject, bool>(info.Get<KeyValuePair<SerializableObject, bool>[]>(nameof(Values)));
}

[Test]
public void DeserializeDictionary()
{
    using (var stream = new MemoryStream())
    {
        var obj1 = new SerializableObject();
        obj1.Values["aaa"] = true;
        var obj2 = new SerializableObject();
        obj2.Values["bbb"] = false;

        var sobj1 = new SerializableObject2
        {
            Values = new Dictionary<SerializableObject, bool>
            {
                [obj1] = true,
                [obj2] = false
            }
        };

        var formatter = new BinaryFormatter();

        formatter.Serialize(stream, sobj1);
        stream.Seek(0, SeekOrigin.Begin);
        sobj1 = (SerializableObject2)formatter.Deserialize(stream);

        Assert.Contains(obj1, sobj1.Values.Keys);
        Assert.Contains(obj2, sobj1.Values.Keys);
    }
}

【问题讨论】:

  • 我知道它总会被调用。问题是当它被调用时,对象还没有被构造,所以 Values 字典是 EMPTY。所以不相等的对象被认为是相等的,因为它们的内容还没有被反序列化!
  • 由于哈希码不能保证稳定,字典不能将它们存储在序列化流中。字典的键被反序列化后,字典需要重新计算哈希值。为此,它实现了IDeserializationCallback,它在对象图反序列化后调用。我不确定是否有任何记录的顺序,当对象图中存在多个此类对象时应该调用 IDeserializationCallback,但 current implementation 按 FIFO 顺序组合它们。

标签: c# dictionary serialization


【解决方案1】:

我不确定,只是推测,但直到被序列化的Dictionary(在本例中为dict)添加它们之后,SerializableObject 才被填充。

您似乎可以通过让SerializableObject 实现ISerializable 并提供您自己的GetObjectData 和反序列化构造函数来解决此问题。

[Serializable]
public class SerializableObject : ISerializable
{
  public SerializableObject()
  {
  }

  public Dictionary<string, bool> Values = new Dictionary<string, bool>();

  public override bool Equals(object obj)
  {
     if (!(obj is SerializableObject other))
        return false;
     if (ReferenceEquals(this, obj))
        return true;
     if (Values.Count != other.Values.Count)
        return false;
     foreach (var kvp in Values)
        if (!other.Values.TryGetValue(kvp.Key, out var otherV) || kvp.Value != otherV)
           return false;
     return true;
  }

  public override int GetHashCode()
  {
     return 0;
  }

  public void GetObjectData(SerializationInfo info, StreamingContext context)
  {
     info.AddValue(KeyValuePairsKey, Values.Select(kvp => kvp).ToArray());
  }

  private SerializableObject(SerializationInfo info, StreamingContext context)
  {
     var kvps = info.GetValue(KeyValuePairsKey, typeof(KeyValuePair<string, bool>[])) as KeyValuePair<string, bool>[];
     foreach (var kvp in kvps)
     {
        Values.Add(kvp.Key, kvp.Value);
     }
  }

  private const string KeyValuePairsKey = "KVPS";
}

class Program
{
  static void Main(string[] args)
  {
     using (var stream = new MemoryStream())
     {

        var obj1 = new SerializableObject();
        obj1.Values["aaa"] = true;
        var obj2 = new SerializableObject();
        obj2.Values["bbb"] = false;

        var formatter = new BinaryFormatter();

        var dict = new Dictionary<SerializableObject, bool>
        {
           [obj1] = true,
           [obj2] = false
        };

        formatter.Serialize(stream, dict);
        stream.Seek(0, SeekOrigin.Begin);
        dict = (Dictionary<SerializableObject, bool>)formatter.Deserialize(stream);
     }
  }
}

【讨论】:

  • 非常感谢,这行得通。反序列化构造函数的代码甚至可以简化:Values = new Dictionary&lt;string, bool&gt;((KeyValuePair&lt;string, bool&gt;[])info.GetValue(KeyValuePairsKey, typeof(KeyValuePair&lt;string, bool&gt;[])));
  • 不幸的是,如果对象是嵌套的,此解决方案将不起作用。我添加了一个更复杂的示例,该示例以 NullReferenceException 失败,因为在创建内部对象之前调用了外部对象的反序列化构造函数。
【解决方案2】:

您总是在 GetHashCode() 方法中返回 0。我认为字典使用 GetHashCode() 来确定键的相等性。

【讨论】:

  • 不,哈希码永远不是相等的唯一来源。如果哈希码不同,那么对象肯定是不同的,不会调用Equals方法。如果哈希码相等,则要么对象相等,要么存在哈希冲突,因此调用Equals 方法来区分这些情况。在序列化之前,字典包含两个对象,因此它们不被认为是相等的。
  • 非常糟糕的做法,但您可以将 GetHashCode 委托给 Values.GetHashCode。非常糟糕的做法,因为值可能会发生变化。
  • 这样,所有对象都将具有准不同的哈希码,因此即使是相等的对象,在将它们放入集合时也会被认为是不相等的。哈希码 0 是正确的,但对性能不利,并且您的建议违反了正确性。
猜你喜欢
  • 1970-01-01
  • 2014-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-24
  • 1970-01-01
  • 2023-03-08
  • 1970-01-01
相关资源
最近更新 更多