Effective C# Item 25: Prefer Serializable Type
在我们创建自己的类型时,有些基本的特性是容易被忽略的,例如可序列化。如果我们的类型不支持可序列化属性,那么对于使用这些类型的开发人员来说可能需要为此付出一些不必要的工作。如果我们的类型不支持序列化,那么对于使用者来说,想要将其修改为支持序列化是非常困难或者根本不可能做到的。
我们应当尽量让我们的自定义类型支持序列化。在.Net中做到这一点是很容易的,大部分情况下只需要添加一个Serializable属性就足够了:
public class MyType
{
private string _label;
private int _value;
}
上例中Serializable属性起作用的原因是类型中的所有成员也都是可序列化类型:string和int都支持序列化。对于下例来说:
public class MyType
{
private string _label;
private int _value;
private OtherClass _object;
}
Serializable属性能否起作用取决于OtherClass是否支持序列化。如果它不支持的化我们就会在运行时遇到错误。为此我们就必须编写自己的代码来序列化这个类。这一工作对于不了解OtherClass内部定义的用户来说,基本是不可能做到的。
.Net序列化将对象中的所有成员变量输出到流中。另外它还支持任意的对象图形:即便是包含循环引用,serialize和deserialize方法都可以保存和复原每一个对象。另外Serializable属性支持二进制和Xml两种序列化方法。
一般来说添加Serializable属性是最简单的支持序列化的方法。但是有些时候这并不是最好的解决方法。例如有时候我并不希望序列化对象中的每一个成员。我们同样可以通过添加属性来解决这个问题。为成员添加[NonSerialized]属性就可以避免它保存在序列化对象中:
public class MyType
{
private string _label;
[NonSerialized]
private int _cachedValue;
private OtherClass _object;
}
当我们反序列化时,这些非序列化对象将不被序列化API初始化。这些被标注[NonSerialized]的成员被初始化为系统默认初始化值:0或者null。如果我们希望修改这些默认的初始值,就需要实现IDeserializationCallBack接口来初始化它们。IDeserializationCallBack接口包含一个方法:OnDeserialization。当完成整个对象图形的反序列化时,系统会调用这个方法。我们可以通过这个方法来初始化那些非序列化的成员。
现在我们已经讨论了为什么我们需要让类型支持序列化以及简单的序列化方法,包括如何初始化非序列化成员。
对于程序中存在的不同版本问题,序列化也有自己的解决方式。当我们面对同一类型的不同版本时,使用Serializable属性生成的代码就会抛出异常。为了在类型的多个版本间实现序列化和反序列化,我们需要实现ISerializable接口。
在下面的例子中我们考虑如何支持MyType的新版本。在新版本中,我们为它添加了一个新成员:
public class MyType
{
private string _label;
[NonSerialized]
private int _value;
private OtherClass _object;
private int _value2 //新加
}
ISerializable接口定义了一个方法,但是我们还需要实现一个序列化构造函数。GetObjectData()方法是ISerializable接口定义的,用来将数据写入流中。另外我们还需要提供一个序列化构造函数来从流中初始化对象:
在下面的类中我们展示了如何通过反序列化构造函数来使用某一类型的新老版本:
{
private string _label;
[NonSerialized]
private int _value;
private const int DEFAULT_VALUE = 5;
private int _value2;
private MyType(SerializationInfo info, StreamingContext cntxt)
{
_label = info.GetString("_label");
try
{
_value2 = info.GetInt32("_value2");
}
catch (SerializationException e)
{
_value2 = DEFAULT_VALUE;
}
}
ISerializable 成员
}
在序列化流中,所有的项都被保存为键/值对。对于使用属性简单标识的类型来说,所有成员变量的名称被做为键。当使用ISerializable接口后,我们可以控制成员变量的声明顺序并使用自定义的键名。
另外我还要强调一下SerializationFormatter的安全权限问题。如果我们没有对GetObjectData做恰当的保护工作,它就有可能成为类的一个安全漏洞。恶意的代码会创建一个StreamingContext,使用GetObjectData从对象中获取值,然后序列化为另一个修改过的SerializationInfo,组成一个修改过的对象。这样一些恶意的使用者就可以在流中修改类的内部成员。我们可以通过SerializationFormatter权限控制来避免这个漏洞。只有那些可信任的代码才能对对象内部成员的状态进行操作。
我们可以看到MyType被声明为sealed,它不能派生子类。在基类中实现ISerializable接口意味着派生自它的所有子类将会变得复杂。所有的派生类必须为反序列化创建构造函数。另外,我们还需要创建GetObjectData方法来向流中添加子类特有的数据。如果我们在上述过程中出现错误,例如一个不适当的反序列化构造函数,编译器是不会捕捉到错误的,只有在运行时才会抛出异常。因此我们推荐尽量在sealed类中实现ISerializable接口。下面我们修改MyType为可序列化的基类。我们需要将序列化构造函数改为protected并且创建一个虚方法(下例中为WriteObjectData方法)来让派生类重写以便储存数据:
{
private string _label;
[NonSerialized]
private int _value;
private const int DEFAULT_VALUE = 5;
private int _value2;
protected MyType(SerializationInfo info, StreamingContext cntxt)
{
_label = info.GetString("_label");
try
{
_value2 = info.GetInt32("_value2");
}
catch (SerializationException e)
{
_value2 = DEFAULT_VALUE;
}
}
ISerializable 成员
protected virtual void WriteObjectData(SerializationInfo inf, StreamingContext cxt)
{
}
}
派生类需要提供自己的序列化构造函数并重写WriteObjectData方法:
{
private int _derivedVal;
private DerivedType(SerializationInfo info, StreamingContext cntxt)
{
_derivedVal = info.GetInt32("_derivedVal");
}
protected override void WriteObjectData(SerializationInfo info, StreamingContext cxt)
{
info.AddValue("_derivedVal", _derivedVal);
}
}
从序列化流中读写值的循序必须是一致的。一般来说是先读写基类的值,因为它的结构相对简单。
.Net Framework为我们提供了一套简单且标准的序列化对象算法。实现序列化可以让我们的类更具持续性,否则有可能给该类的用户造成不必要的麻烦。一般情况下我们可以使用默认的方法进行序列化标记。当默认方法不能满足我们的要求时,可以通过实现ISerializable接口来达到目的。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
回到目录