.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接口,如果是就需要特殊处理它的序列化过程。