11.9 [Serializable] and ISerializable
Eagle-eyed readers will have noticed that in all the preceding
examples the classes are sealed. This is no
accident. Marking a class as [Serializable] and
deciding whether to implement ISerializable has
some specific implications for both base and derived types, and
deserves separate discussion.
Consider the following serializable class hierarchy:
[Serializable]
public class Person {
public string Name;
public int Age;
// Rest of class...
}
[Serializable]
public sealed class Student: Person {
public string Course;
// Rest of class...
}
In this example both Person and
Student are serializable, and both classes use the
default runtime serialization behavior since neither class implements
ISerializable.
Now imagine that the developer of Person decides
for some reason to implement ISerializable and
provide a deserialization constructor to control
Person serialization. The new version of
Person might look like this:
[Serializable]
public class Person : ISerialization {
public string Name;
public int Age;
public void GetObjectData(SerializationInfo si, StreamingContext sc) {
si.AddValue("IChangedTheNameFieldName", Name);
si.AddValue("IChangedTheAgeFieldName", Age);
}
protected Person(SerializationInfo si, StreamingContext sc) {
Name = si.GetString("IChangedTheNameFieldName");
Age = sc.GetInt32("IChangedTheAgeFieldName");
}
// Rest of class...
}
Although this works for instances of Person, this
change breaks serialization of Student instances.
Serializing a Student instance would appear to
succeed, but the Course field in the
Student type isn't saved to the
stream because the implementation of
ISerializable.GetObjectData on
Person has no knowledge of the members of the
Student derived type. Additionally,
deserialization of Student instances throws an
exception since the runtime is looking (unsuccessfully) for a
deserialization constructor on Student.
Fundamentally, if a base class implements
ISerializable and the other serialization-related
interfaces and base classes, derived classes need to follow suit.
There are a several ways of accomplishing this. One option is for the
base class to declare GetObjectData as
virtual, allowing derived classes to simply
override it as needed and call up the base type's
GetObjectData as needed. Another option is for the
derived class to reimplement ISerializable using
explicit interface implementation, calling up to the base
implementation as needed. In all cases, derived types need to provide
deserialization constructors, and chain up to the base
deserialization constructor as needed. An implementation of
Student that takes the latter approach looks like
this:
[Serializable]
public sealed class Student : Person, ISerialization {
public string Course;
void ISerialization.GetObjectData(SerializationInfo si,
StreamingContext sc) {
si.AddValue("IChangedTheCourseFieldName", Course);
base.GetObjectData(si, sc);
}
private Student(SerializationInfo si, StreamingContext sc)
: base(si, sc) {
Course = si.GetString("IChangedTheCourseFieldName");
}
// Rest of class...
}
Now imagine a case in which the developer of
Person elects not to implement
ISerializable, relying instead on the default
runtime serialization behavior. A derived type such as
Student wishing to control its serialization by
implementing ISerializable needs to ensure that
the Person members are correctly serialized as
part of the GetObjectData call, and correctly
initialized by the Student deserialization
constructor. While this could be performed manually in the derived
type for public and protected
members of the base type, the problem remains for
private members of the base type that need
serialization and deserialization.
The problem is that Person does not have an
ISerializable.GetObjectData method or a
deserialization constructor that the Student type
can delegate to. The solution to this problem is the
FormatterServices class, which provides a standard
way to retrieve the list of all members in a type that need to be
serialized (GetSerializableMembers), as well as a
standard way to retrieve a serialized representation of these members
(GetObjectData), and a standard way to deserialize
this representation back into a new instance
(PopulateObjectMembers). An implementation of
Student that uses
FormatterServices looks like this:
[Serializable]
public sealed class Student : Person, ISerializable {
public string Course;
public void GetObjectData(SerializationInfo si, StreamingContext sc) {
Type t = typeof(Person);
MemberInfo[ ] mbrs = FormatterServices.GetSerializableMembers(t);
object[ ] data = FormatterServices.GetObjectData(this, mbrs);
si.AddValue("BaseData", data);
si.AddValue("IChangedTheCourseFieldName", Course);
}
private Student(SerializationInfo si, StreamingContext sc) {
Type t = typeof(Person);
MemberInfo[ ] mbrs = FormatterServices.GetSerializableMembers( );
object[ ] data = (object[ ])si.GetValue("BaseData", typeof(object[ ]));
FormatterServices.PopulateObjectMembers(this, mbrs, data);
Course = si.GetString("IChangedTheCourseFieldName");
}
// Rest of class...
}
|