.NET:CLR via C#:Runtime Serialization
Making a Type Serializable
The SerializableAttribute custom attribute may be applied to reference types (class), value types (struct), enumerated types (enum), and delegate types (delegate) only. (Note that enumerated and delegate types are always serializable, so there is no need to explicitly apply the SerializableAttribute attribute to these types.) In addition, the SerializableAttribute attribute is not inherited by derived types.
Do not use C#’s automatically implemented property feature to define properties inside types marked with the [Serializable] attribute, because the compiler generates the names of the fields and the generated names can be different each time that you recompile your code, preventing instances of your type from being deserializable.
Use of the System.Runtime.Serialization.OnDeserialized custom attribute is the preferred way of invoking a method when an object is deserialized, as opposed to having a type implement the System.Runtime.Serialization. IDeserializationCallback interface’s OnDeserialization method.
When you are serializing a set of objects, the formatter first calls all of the objects’ methods that are marked with the OnSerializing attribute. Next, it serializes all of the objects’ fields, and finally it calls all of the objects’ methods marked with the OnSerialized attribute. Similarly, when you deserialize a set of objects, the formatter calls all of the objects’ methods that are marked with the OnDeserializing attribute, then it deserializes all of the object’s fields, and then it calls all of the objects’ methods marked with the OnDeserialized attribute.
during deserialization, when a formatter sees a type offering a method marked with the OnDeserialized attribute, the formatter adds this object’s reference to an internal list. After all the objects have been deserialized, the formatter traverses this list in reverse order and calls each object’s OnDeserialized method. When this method is called, all the serializable fields will be set correctly, and they may be accessed to perform any additional work that would be necessary to fully deserialize the object. Invoking these methods in reverse order is important because it allows inner objects to finish their deserialization before the outer objects that contain them finish their deserialization.
If you serialize an instance of a type, add a new field to the type, and then try to deserialize the object that did not contain the new field, the formatter throws a SerializationException with a message indicating that the data in the stream being deserialized has the wrong number of members. This is very problematic in versioning scenarios where it is common to add new fields to a type in a newer version. Fortunately, you can use the System.Runtime.Serialization.OptionalFieldAttribute attribute to help you.
You apply the OptionalFieldAttribute attribute to each new field you add to a type. Now, when the formatters see this attribute applied to a field, the formatters will not throw the SerializationException exception if the data in the stream does not contain the field.
测试代码
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Runtime.Serialization; 7 using System.Runtime.Serialization.Formatters.Binary; 8 using System.IO; 9 10 namespace SerializationStudy 11 { 12 class AttributeSerializable 13 { 14 public static void Test() 15 { 16 using (MemoryStream stream = new MemoryStream()) 17 { 18 var formater = new BinaryFormatter(); 19 formater.Serialize(stream, new TestClassA() { A1 = new TestClassA1 { X = 5 } }); 20 21 stream.Position = 0; 22 23 var test = formater.Deserialize(stream) as TestClassA; 24 25 Console.WriteLine(test.A1.X); 26 } 27 } 28 } 29 30 [Serializable] 31 public sealed class TestClassA 32 { 33 public TestClassA1 A1 = new TestClassA1(); 34 35 [OnSerializing] 36 private void OnSerializing(StreamingContext context) 37 { 38 Console.WriteLine("TestClassA OnSerializing"); 39 } 40 41 [OnSerialized] 42 private void OnSerialized(StreamingContext context) 43 { 44 Console.WriteLine("TestClassA OnSerialized"); 45 } 46 47 [OnDeserializing] 48 private void OnDeserializing(StreamingContext context) 49 { 50 Console.WriteLine("TestClassA OnDeserializing"); 51 } 52 53 [OnDeserialized] 54 private void OnDeserialized(StreamingContext context) 55 { 56 Console.WriteLine("TestClassA OnDeserialized"); 57 } 58 } 59 60 [Serializable] 61 public sealed class TestClassA1 62 { 63 public int X; 64 65 [OnSerializing] 66 private void OnSerializing(StreamingContext context) 67 { 68 Console.WriteLine("TestClassA1 OnSerializing"); 69 } 70 71 [OnSerialized] 72 private void OnSerialized(StreamingContext context) 73 { 74 Console.WriteLine("TestClassA1 OnSerialized"); 75 } 76 77 [OnDeserializing] 78 private void OnDeserializing(StreamingContext context) 79 { 80 Console.WriteLine("TestClassA1 OnDeserializing"); 81 } 82 83 [OnDeserialized] 84 private void OnDeserialized(StreamingContext context) 85 { 86 Console.WriteLine("TestClassA1 OnDeserialized"); 87 } 88 } 89 }
运行效果
Controlling the Serialized/Deserialized Data
To get complete control over what data is serialized/deserialized or to eliminate the use of reflection,your type can implement the System.Runtime.Serialization.ISerializable interface, which is defined as follows.
1 public interface ISerializable 2 { 3 void GetObjectData(SerializationInfo info, StreamingContext context); 4 }
When a formatter serializes an object graph, it looks at each object. If its type implements the ISerializable interface, then the formatter ignores all custom attributes and instead constructs a new System.Runtime.Serialization.SerializationInfo object. This object contains the actual set of values that should be serialized for the object.
Although you can set a SerializationInfo’s FullTypeName and AssemblyName properties, this is discouraged. If you want to change the type that is being serialized, it is recommended that you call SerializationInfo’s SetType method, passing a reference to the desired Type object. Calling SetType ensures that the type’s full name and defining assembly are set correctly.
After the SerializationInfo object is constructed and initialized, the formatter calls the type’s GetObjectData method, passing it the reference to the SerializationInfo object. The GetObjectData method is responsible for determining what information is necessary to serialize the object and adding this information to the SerializationInfo object. GetObjectData indicates what information to serialize by calling one of the many overloaded AddValue methods provided by the SerializationInfo type. AddValue is called once for each piece of data that you want to add.
You should always call one of the overloaded AddValue methods to add serialization information for your type. If a field’s type implements the ISerializable interface, don’t call the GetObjectData on the field. Instead, call AddValue to add the field; the formatter will see that the field’s type implements ISerializable and the formatter will call GetObjectData for you. If you were to call GetObjectData on the field object, the formatter wouldn’t know to create a new object when deserializing the stream.
The formatter takes all of the values added to the SerializationInfo object and serializes each of them out to the stream.
let’s turn our attention to deserialization. As the formatter extracts an object from the stream, it allocates memory for the new object (by calling the System.Runtime.Serialization.FormatterServices type’s static GetUninitializedObject method). Initially, all of this object’s fields are set to 0 or null. Then, the formatter checks if the type implements the ISerializable interface. If this interface exists, the formatter attempts to call a special constructor whose parameters are identical to that of the GetObjectData method.
This constructor receives a reference to a SerializationInfo object containing all of the values added to it when the object was serialized. The special constructor can call any of the GetBoolean, GetChar, GetByte, GetSByte, GetInt16, GetUInt16, GetInt32, GetUInt32, GetInt64, GetUInt64, GetSingle, GetDouble, GetDecimal, GetDateTime, GetString, and GetValue methods, passing in a string corresponding to the name used to serialize a value. The value returned from each of these methods is then used to initialize the fields of the new object.
when a SerializationInfo object is constructed, it is passed an object whose type implements the IFormatterConverter interface. Because the formatter is responsible for constructing the SerializationInfo object, it chooses whatever IFormatterConverter type it wants. Microsoft’s BinaryFormatter and SoapFormatter types always construct an instance of the System.Runtime.Serialization.FormatterConverter type. Microsoft’s formatters don’t offer any way for you to select a different IFormatterConverter type.
The FormatterConverter type calls the System.Convert class’s static methods to convert values between the core types, such as converting an Int32 to an Int64. However, to convert a value between other arbitrary types, the FormatterConverter calls Convert’s ChangeType method to cast the serialized (or original) type to an IConvertible interface and then calls the appropriate interface method. Therefore, to allow objects of a serializable type to be deserialized as a different type, you may want to consider having your type implement the IConvertible interface. Note that the FormatterConverter object is used only when deserializing objects and when you’re calling a Get method whose type doesn’t match the type of the value in the stream.
The code in the special constructor typically extracts its fields from the SerializationInfo object that is passed to it. As the fields are extracted, you are not guaranteed that the objects are fully deserialized, so the code in the special constructor should not attempt to manipulate the objects that it extracts. If your type must access members (such as call methods) on an extracted object, then it is recommended that your type also provide a method that has the OnDeserialized attribute applied to it or have your type implement the IDeserializationCallback interface’s OnDeserialization method (as shown in the Dictionary example). When this method is called, all objects have had their fields set. However, there is no guarantee to the order in which multiple objects have their OnDeserialized or OnDeserialization method called. So, although the fields may be initialized, you still don’t know if a referenced object is completely deserialized if that referenced object also provides an OnDeserialized method or implements the IDeserializationCallback interface.
测试代码
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Runtime.Serialization; 7 using System.Runtime.Serialization.Formatters.Binary; 8 using System.IO; 9 10 namespace SerializationStudy 11 { 12 class InterfaceSerializable 13 { 14 public static void Test() 15 { 16 using (MemoryStream stream = new MemoryStream()) 17 { 18 var formater = new BinaryFormatter(); 19 formater.Serialize(stream, new TestClassB() { X = 5 }); 20 21 stream.Position = 0; 22 23 var test = formater.Deserialize(stream) as TestClassB; 24 25 Console.WriteLine(test.X); 26 } 27 } 28 } 29 30 [Serializable] 31 public sealed class TestClassB : ISerializable, IDeserializationCallback 32 { 33 public int X; 34 35 public TestClassB() 36 { 37 } 38 39 private TestClassB(SerializationInfo info, StreamingContext context) 40 { 41 X = info.GetInt32("X"); 42 } 43 44 public void GetObjectData(SerializationInfo info, StreamingContext context) 45 { 46 info.AddValue("X", X); 47 } 48 49 [OnSerializing] 50 private void OnSerializing(StreamingContext context) 51 { 52 Console.WriteLine("TestClassB OnSerializing"); 53 } 54 55 [OnSerialized] 56 private void OnSerialized(StreamingContext context) 57 { 58 Console.WriteLine("TestClassB OnSerialized"); 59 } 60 61 [OnDeserializing] 62 private void OnDeserializing(StreamingContext context) 63 { 64 Console.WriteLine("TestClassB OnDeserializing"); 65 } 66 67 [OnDeserialized] 68 private void OnDeserialized(StreamingContext context) 69 { 70 Console.WriteLine("TestClassB OnDeserialized"); 71 } 72 73 public void OnDeserialization(object sender) 74 { 75 Console.WriteLine("TestClassB OnDeserialization"); 76 } 77 } 78 }
运行结果
Serializing a Type As a Different Type and Deserializing an Object As a Different Object
你可以在 GetObjectData 中调用 SerializationInfo.SetType 修改反序列时创建的类型信息,如果反序列时,创建的类型实现了 System.Runtime.Serialization.IObjectReference 接口:
1 public interface IObjectReference 2 { 3 Object GetRealObject(StreamingContext context); 4 }
When a type implements this interface, the formatter calls the GetRealObject method. This method returns a reference to the object that you really want a reference to now that deserialization of the object has completed.
测试代码
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Runtime.Serialization; 7 using System.Runtime.Serialization.Formatters.Binary; 8 using System.IO; 9 10 namespace SerializationStudy 11 { 12 class ObjectReference 13 { 14 public static void Test() 15 { 16 using (MemoryStream stream = new MemoryStream()) 17 { 18 var formater = new BinaryFormatter(); 19 formater.Serialize(stream, new TestClassC()); 20 21 stream.Position = 0; 22 23 var test = formater.Deserialize(stream); 24 25 Console.WriteLine(test); 26 } 27 } 28 } 29 30 [Serializable] 31 public class TestClassC : IObjectReference 32 { 33 public object GetRealObject(StreamingContext context) 34 { 35 return "变成字符串了"; 36 } 37 } 38 }
输出结果
Serialization Surrogates
Up to now, I’ve been discussing how to modify a type’s implementation to control how a type serializes and deserializes instances of itself. However, the formatters also allow code that is not part of the type’s implementation to override how a type serializes and deserializes its objects.
A serialization surrogate type must implement the System.Runtime.Serialization.ISerializationSurrogate interface, which is defined in the FCL as follows.
1 public interface ISerializationSurrogate 2 { 3 void GetObjectData(Object obj, SerializationInfo info, StreamingContext context); 4 Object SetObjectData(Object obj, SerializationInfo info, StreamingContext context, 5 ISurrogateSelector selector); 6 }
还没看懂的一段话
The BinaryFormatter class has a bug that prevents a surrogate from serializing objects with references to each other. To fix this problem, you need to pass a reference to your ISerializationSurrogate object to FormatterServices’s static GetSurrogateForCyclicalReference method. This method returns an ISerializationSurrogate object, which you can then pass to the SurrogateSelector’s AddSurrogate method. However, when you use the GetSurrogateForCyclicalReference method, your surrogate’s SetObjectData method must modify the value inside the object referred to by SetObjectData’s obj parameter and ultimately return null or obj to the calling method. The downloadable code that accompanies this book shows how to modify the UniversalToLocalTimeSerializationSurrogate class and the SerializationSurrogateDemo method to support cyclical references.
Overriding the Assembly and/or Type When Deserializing an Object
The System.Runtime.Serialization.SerializationBinder class makes deserializing an object to a different type very easy.
The SerializationBinder class also makes it possible to change the assembly/type information while serializing an object by overriding its BindToName method, which looks like this.
During serialization, the formatter calls this method, passing you the type it wants to serialize. You can then return (via the two out parameters) the assembly and type that you want to serialize instead. If you return null and null (which is what the default implementation does), then no change is performed.