.NET陷阱之一:IDeserializationCallback带来的问题

代码中有一个类,其中包含一个字典(Dictionary<Key, Value>),本来想让前者实现IDeserializationCallback接口,以便在反序列化时根据字典的内容做一些初始化工作,结果循环字典元素的代码就是不走。费了好大劲才找到原因,先来看有问题的代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.IO;
 4 using System.Runtime.Serialization;
 5 using System.Runtime.Serialization.Formatters.Binary;
 6 
 7 namespace DotNetBugs
 8 {
 9     [Serializable]
10     public class Example : IDeserializationCallback
11     {
12         private Dictionary<string, string> map = new Dictionary<string, string>();
13 
14         public Example()
15         {
16             map.Add("one", "1");
17             map.Add("two", "2");
18         }
19 
20         public void OnDeserialization(object sender)
21         {
22             Dump();
23         }
24 
25         public void Dump()
26         {
27             foreach (var item in map)
28             {
29                 Console.WriteLine(item.Key + " -> " + item.Value);
30             }
31         }
32     }
33 
34     public class Starter
35     {
36         public static void Main(string[] args)
37         {
38             using (var stream = new MemoryStream())
39             {
40                 var formatter = new BinaryFormatter();
41                 formatter.Serialize(stream, new Example());
42 
43                 stream.Seek(0, SeekOrigin.Begin);
44                 var example = (Example)formatter.Deserialize(stream);
45 
46                 Console.WriteLine("after deserialize");
47                 example.Dump();
48 
49                 Console.Read();
50             }
51         }
52     }
53 }

你期望控制台有什么样的输出呢,是不是这样?

one -> 1               |
two -> 2               | 在第44行反序列化时,Example.OnDeserialization中调用Dump的输出。
after deserialize
one -> 1               |
two -> 2               | 在第47行调用Dump的输出

但实际的输出内容是:

after deserialize
one -> 1
two -> 2

为什么会这样呢?

来看一下Dictionary<Key, Value>的源代码(通过.NET Reflector反编译得到,代码已经简化,只显示与此问题相关的部分 ):

 1 [Serializable]
 2 public class Dictionary<TKey, TValue> : ISerializable, IDeserializationCallback
 3 {
 4     private SerializationInfo m_siInfo;
 5     
 6     protected Dictionary(SerializationInfo info, StreamingContext context)
 7     {
 8         this.m_siInfo = info;
 9     }
10     
11     public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
12     {
13         info.AddValue("Version", this.version);
14         info.AddValue("Comparer", this.comparer, typeof(IEqualityComparer<TKey>));
15         info.AddValue("HashSize", (this.buckets == null) ? 0 : this.buckets.Length);
16         if (this.buckets != null)
17         {
18             KeyValuePair<TKey, TValue>[] array = new KeyValuePair<TKey, TValue>[this.Count];
19             this.CopyTo(array, 0);
20             info.AddValue("KeyValuePairs", array, typeof(KeyValuePair<TKey, TValue>[]));
21         }
22     }
23 
24     public virtual void OnDeserialization(object sender)
25     {
26         if (this.m_siInfo != null)
27         {
28             int num = this.m_siInfo.GetInt32("Version");
29             int num2 = this.m_siInfo.GetInt32("HashSize");
30             this.comparer = (IEqualityComparer<TKey>)this.m_siInfo.GetValue(
"Comparer", typeof(IEqualityComparer<TKey>));
31 if (num2 != 0) 32 { 33 this.buckets = new int[num2]; 34 for (int i = 0; i < this.buckets.Length; i++) 35 { 36 this.buckets[i] = -1; 37 } 38 this.entries = new Entry<TKey, TValue>[num2]; 39 this.freeList = -1; 40 KeyValuePair<TKey, TValue>[] pairArray = (KeyValuePair<TKey, TValue>[])
this.m_siInfo.GetValue("KeyValuePairs",
typeof(KeyValuePair<TKey, TValue>[])); 41 if (pairArray == null) 42 { 43 ThrowHelper.ThrowSerializationException(
ExceptionResource.Serialization_MissingKeyValuePairs);
44 } 45 for (int j = 0; j < pairArray.Length; j++) 46 { 47 if (pairArray[j].Key == null) 48 { 49 ThrowHelper.ThrowSerializationException(
ExceptionResource.Serialization_NullKey);
50 } 51 this.Insert(pairArray[j].Key, pairArray[j].Value, true); 52 } 53 } 54 else 55 { 56 this.buckets = null; 57 } 58 this.version = num; 59 this.m_siInfo = null; 60 } 61 } 62 } 63

原来Dictionary<Key, Value>在内部是通过数组的形式将自己的内容序列化到流中的,它也实现了IDeserializationCallback接口,用于在反序列化时重新构建字典。

问题就在这里——在Example类的对象被反序列化时,对象图中一共有两个实现IDeserializationCallback接口的对象,而且从结果来看,Example.map的OnDeserialization方法是在Example类对象之后被调用的,所以Example.OnDeserialization调用时map中还没有任何内容!

所以要解决这一问题,我们需要将代码改成下面那样:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.IO;
 4 using System.Runtime.Serialization;
 5 using System.Runtime.Serialization.Formatters.Binary;
 6 
 7 namespace dotNetBugs
 8 {
 9     [Serializable]
10     public class Example : ISerializable, IDeserializationCallback
11     {
12         [NonSerialized]
13         private Dictionary<string, string> map = new Dictionary<string, string>();
14 
15         public Example()
16         {
17             map.Add("one", "1");
18             map.Add("two", "2");
19         }
20 
21         private Example(SerializationInfo info, StreamingContext context)
22         {
23             var keys = (string[])info.GetValue("MapKeys", typeof(object));
24             var vals = (string[])info.GetValue("MapVals", typeof(object));
25             map = new Dictionary<string, string>();
26             for (int i = 0; i < keys.Length; ++i)
27             {
28                 map.Add(keys[i], vals[i]);
29             }
30         }
31 
32         public void OnDeserialization(object sender)
33         {
34             Dump();
35         }
36 
37         public void Dump()
38         {
39             foreach (var item in map)
40             {
41                 Console.WriteLine(item.Key + " -> " + item.Value);
42             }
43         }
44 
45         public void GetObjectData(SerializationInfo info, StreamingContext context)
46         {
47             var keys = new string[map.Count];
48             var vals = new string[map.Count];
49             int i = 0;
50             foreach (var item in map)
51             {
52                 keys[i] = item.Key;
53                 vals[i] = item.Value;
54                 ++i;
55             }
56 
57             info.AddValue("MapKeys", keys);
58             info.AddValue("MapVals", vals);
59         }
60     }
61 
62     public class Starter
63     {
64         public static void Main(string[] args)
65         {
66             using (var stream = new MemoryStream())
67             {
68                 var formatter = new BinaryFormatter();
69                 formatter.Serialize(stream, new Example());
70 
71                 stream.Seek(0, SeekOrigin.Begin);
72                 var example = (Example)formatter.Deserialize(stream);
73 
74                 Console.WriteLine("after deserialize");
75                 example.Dump();
76 
77                 Console.Read();
78             }
79         }
80     }
81 }

这样,输出就像预料的一样了。

总结一下:如果某个类Outer实现了IDeserializationCallback接口,而且OnDeserialization方法中的逻辑依赖于Outer类的某个成员inner,则一定检查inner是否也实现了IDeserializationCallback接口,如果是就需要特殊处理它的序列化过程。

原文地址:https://www.cnblogs.com/brucebi/p/2993968.html