Dictionary empty for deserialization

I'm currently writing a bidirectional map class, and I'm having problems serializing / deserializing the class (question below).

Here are parts of the corresponding class.

/// <summary> /// Represents a dictionary where both keys and values are unique, and the mapping between them is bidirectional. /// </summary> /// <typeparam name="TKey"> The type of the keys in the dictionary. </typeparam> /// <typeparam name="TValue"> The type of the values in the dictionary. </typeparam> [Serializable] public class BidirectionalDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IEquatable<BidirectionalDictionary<TKey, TValue>>, ISerializable, IDeserializationCallback { /// <summary> /// A dictionary that maps the keys to values. /// </summary> private readonly Dictionary<TKey, TValue> forwardMap; /// <summary> /// A dictionary that maps the values to keys. /// </summary> private readonly Dictionary<TValue, TKey> inverseMap; /// <summary> /// An instance of the dictionary where the values are the keys, and the keys are the values. /// </summary> private readonly BidirectionalDictionary<TValue, TKey> inverseInstance; /// <summary> /// Initializes a new instance of the dictionary class with serialized data. </summary> /// </summary> /// <param name="info"> The serialization info. </param> /// <param name="context"> The sserialization context. </param> protected BidirectionalDictionary(SerializationInfo info, StreamingContext context) { this.forwardMap = (Dictionary<TKey, TValue>)info.GetValue("UnderlyingDictionary", typeof(Dictionary<TKey, TValue>)); this.inverseMap = new Dictionary<TValue, TKey>( forwardMap.Count, (IEqualityComparer<TValue>)info.GetValue("InverseComparer", typeof(IEqualityComparer<TValue>))); // forwardMap is always empty at this point. foreach (KeyValuePair<TKey, TValue> entry in forwardMap) inverseMap.Add(entry.Value, entry.Key); this.inverseInstance = new BidirectionalDictionary<TValue, TKey>(this); } /// <summary> /// Gets the data needed to serialize the dictionary. /// </summary> /// <param name="info"> The serialization info. </param> /// <param name="context"> The serialization context. </param> public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("UnderlyingDictionary", forwardMap); info.AddValue("InverseComparer", inverseMap.Comparer); } } 

Since the forward and inverseMap dictionaries contain the same data, my idea was to only serialize one of them (forwardMap), and then build another (inverseMap) data from it during deserialization. However, the inverseMap is not populated with any data in the deserialization constructor. It seems the forwardMap dictionary is completely deserialized after the class deserialization constructor has already completed.

Any idea on how to fix this?

+5
source share
1 answer

I assume you are using BinaryFormatter .

BinaryFormatter - graph serializer. Instead of objects being stored in a clean tree, they are assigned temporary object identifiers and stored in the form in which they occur. Thus, when an object is deserialized, it is not guaranteed that all referenced objects were previously deserialized. Thus, the entries in your forwardMap are not populated yet.

The usual workaround is to add an IDeserializationCallback to your class and build your inverseMap and inverseInstance after everything has been deserialized in the OnDeserialization method. But Dictionary<TKey, TValue> also implements IDeserializationCallback , which introduces an additional sequencing problem: it was not guaranteed to be called before yours. In this thread, Microsoft writes :

Objects are reconstructed internally, and invocation methods during deserialization may have undesirable side effects, since invoked methods may refer to references to objects that were not deserialized by the time the call ended. If the deserialized class implements IDeserializationCallback, the OnSerialization method will be automatically called when the entire object graph is deserialized. At this point, all the referenced child objects have been fully restored. A hash table is a typical example of a class that is difficult to deserialize without using the event listener described above. It is easy to get key / value pairs during deserialization, but adding these objects back to the hash table can cause problems because there is no guarantee that classes derived from the hash table are deserialized. Therefore, calling methods in a hash table at this stage are impractical.

So you can do a couple of things:

  • Instead of saving Dictionary<TKey,TValue> , save the array KeyValuePair<TKey,TValue> . This has the advantage that your binary data is simpler, but you need to allocate an array in your GetObjectData() method.

  • Or follow the recommendations in the dictionary reference source:

     // It might be necessary to call OnDeserialization from a container if the container object also implements // OnDeserialization. However, remoting will call OnDeserialization again. // We can return immediately if this function is called twice. // Note we set remove the serialization info from the table at the end of this method. 

    those. in your callback, call the OnDeserialization method of your nested dictionary before using it:

     public partial class BidirectionalDictionary<TKey, TValue> : IDeserializationCallback { public void OnDeserialization(object sender) { this.forwardMap.OnDeserialization(sender); foreach (KeyValuePair<TKey, TValue> entry in forwardMap) { this.inverseMap.Add(entry.Value, entry.Key); } // inverseInstance will no longer be able to be read-only sicne it is being allocated in a post-deserialization callback. this.inverseInstance = new BidirectionalDictionary<TValue, TKey>(this); } 

    (If you prefer, you can do this in [OnDeserialied] .)

By the way, this blog post claims that it is safe to call the OnDeserialization HashTable method from the deserialization constructor containing the class, and not later OnDeserialization , so you can try it.

+7
source

Source: https://habr.com/ru/post/1201470/


All Articles