Effective C# 原则25: 让你的类型支持序列化
对象的持久是类型的一个核心功能。这是一个在你忽略对它的支持以前,没有人会注意到的基本元素之一。
如果你的类型不能恰当的支持序列化,那么对于把你类的做为基类或者成员的开发人员来说,你会给他们增加很多的工作量。当你的类型不支持序列化时,他们不得不围绕这工作,自己添加实现这个标准的功能。而对于不能访问类的私有成员的开发人来说,恰当的实现你的类型的序列化是不太可能的。如果你的类型不支持序列化,那么对于你的用户来说,想再要实现实它是很困难或者根本就不可能的事。
取而代之的是,为你的实际类型添加序列化。对于那些不用承载UI元素,窗口,或者表单的类型来说这是有实际意义的。感觉有额外的工作是没有理由的,.Net的序列化是很简单的,以至于你没有任意的借口说不支持它。在多数情况下,添加Serializable特性就足够了:
[Serializable]
public class MyType
{
private string _label;
private int _value;
}
只添加一个Serializable特性就足以让它可以序列化,是因为这它的成员都是可序列化的:string和int都是.Net序列化支持的。无论是否可能,都要给类型添加序列化支持是很重要的,原因在你添加另一个类做为新类的成员时就很明显了:
[Serializable]
public class MyType
{
private string _label;
private int _value;
private OtherClass _object;
}
这里的Serializable特性只有在OtherClass也支持序列化时才有效。如果OtherClass不支持序列化,那么你在序列化MyType时,因为OtherClass对象也在里面,你会得到一个运行时错误。这只是因为对OtherClass的内部结构不清楚,而使序列化成为不可能。
.Net的序列化是把类中所有成员变量保存到输出流中。另外,.Net的序列化还支持任意的对象图(object
graph):即使你的对象上有一个循环引用,serialize 和deserialize
方法都只会为你的实际对象读取和储存一次。当一些web对象反序列化了以后,.Net序列化框架也可以创建这些web对象的引用。你创建的任何与web相关的对象,在对象图序列化以后,你都可以正确的保存它们。最后一个要注意的地方是Serializable
特性同时支持二进制和SOAP序列化。这一原则里的所有技术都支持这两种序列化格式。但是要记住:只有当所有类型的对象图都支持序列化时才能成功。这就是为什么要让所有的类型都支持序列化显得很重要了。一但你放过了一个类,你就轻意的给对象图开了个后门,以至于所有使用这个类的人,想要序列化对象图时变得更加困难。不久以后,他们就会发现不得不自己写序列化代码了。
添加Serializable特性是一个最简单的技术来支持对象的序列化。但最简单的方案并不是总是正确的方案。有时候,你并不想序列化对象的所有成员:有些成员可能只存在于长期操作的缓存中,还有一些对象可能占用着一些运行时资源,而这些资源只能存在于内存中。你同样可以很好的使用特性来控制这些问题。添加NonSerialized特性到任何你不想让它序列化的数据成员上。这给它们标上了不用序列化的标记:
Serializable]
public class MyType
{
private string _label;
[NonSerialized]
private int _cachedValue;
private OtherClass _object;
}
你,做为类的设计者,非序列化成员给你多添加了一点点工作。在序列化过程中,序列化API不会为你初始化非序列化成员。因为类型的构造函数没有被调用,所以成员的初始化也不会被执行。当你使用序列化特性时,非序列成员就保存着系统默认值:0或者null。当默认的0对初始化来说是不正确的,那么你须要实现IDeserializationCallback
接口,来初始化这些非序列化成员。框架会在整个对象图反序列化以后,调用这个方法。这时,你就可以用它为所有的非序列化成员进行初始化了。因为整个对象图已经载入,所以你的类型上的所有方法的调用及成员的使用都是安全的。不幸的是,这不是傻瓜式的。在整个对象图载入后,框架会在对象图中每个实现了IDeserializationCallback接口的对象上调用OnDeserialization方法。对象图中的其它任何对象可以在OnDeserialization正在进行时调用对象的公共成员。如果它们抢在了前面,那么你的非序列化成员就是null或者0。顺序是无法保证的,所以你必须确保你的所有公共成员,都能处理非序列化成员还没有初始化的这种情况。
到目前为止,你已经知道为什么要为所有类型添加序列化了:非序列化类型会在要序列化的对象中使用时带来更多的麻烦事。你也学会了用特性来实现最简单的序列化方法,还包括如何初始化非序列化成员。
序列化了对象有方法在程序的不同版本间生存。(译注:这是一个很重要的问题,因为.Net里的序列化不像C++那样,你可以轻松的自己控制每一个字节的数据,因此版本问题成了序列化中经常遇到的一个问题。)
添加序列化到一个类型上,就意味着有一天你要读取这个对象的早期版本。Serializable特性生成的代码,在对象图的成员被添加或者移除时会抛出异常。当你发现你自己已经要面对多版本问题时,你就需要在序列化过程中负出更多的操作:使用ISerializable接口。这个接口定义了一些hook用于自定义序列化你的类型。ISerializable接口里使用的方法和存储与默认的序列化方法和储存是一致的,这就是说,你可以使用序列化特性。如果什么时候有必要提供你自己的扩展序列化时,你可以再添加对ISerializable接口的支持。
做一个为例子:考虑你如何来支持MyType的第2个版本,也就是添加了另一个字段到类中时。简单的添加一个字段都会产生一个新的类型,而这与先前已经存在磁盘上的版本是不兼容的:
[Serializable]
public class MyType
{
private string _label;
[NonSerialized]
private int _value;
private OtherClass _object;
// Added in version 2
// The runtime throws Exceptions
// with it finds this field missing in version 1.0
// files.
private int _value2;
}
你实现ISerializable接口来支持对这个行为的处理。ISerializable接口定义了一个方法,但你必需实现两个。ISerializable定义了GetObjectData()方法,这是用于写数据到流中。另外,如果你必须提供一个序列析构函数从流中初始化对象:
private MyType( SerializationInfo info, StreamingContext cntxt );
下面的序列化构造函数演示了如何从先前的版本中读取数据,以及和默认添加的Serializable特性生成的序列化保持供一致,来读取当前版本中的数据:
using System.Runtime.Serialization;
using System.Security.Permissions;
[Serializable]
public sealed class MyType : ISerializable
{
private string _label;
[NonSerialized]
private int _value;
private OtherClass _object;
private const int DEFAULT_VALUE = 5;
private int _value2;
// public constructors elided.
// Private constructor used only by the Serialization
framework.
private MyType( SerializationInfo info,
StreamingContext cntxt )
{
_label = info.GetString( "_label" );
_object = ( OtherClass )info.GetValue( "_object", typeof
( OtherClass ));
try {
_value2 = info.GetInt32( "_value2" );
} catch ( SerializationException e )
{
// Found version 1.
_value2 = DEFAULT_VALUE;
}
}
[SecurityPermissionAttribute(SecurityAction.Demand,
SerializationFormatter =true)]
void ISerializable.GetObjectData (SerializationInfo inf,
StreamingContext cxt)
{
inf.AddValue( "_label", _label );
inf.AddValue( "_object", _object );
inf.AddValue( "_value2", _value2 );
}
}
序列化流是以键/值对应的方法来保存每一个元素的。默认的特性生成的代码是以变量名做为键来存储值。当你添加了ISerializable接口后,你必须匹配键名以及变量顺序。这个顺序就是在类中定义时的顺序。(顺便说一句,这实际上就是说重新排列类中的变量名或者重新给变量命名,都会破坏对已经创建了的文件的兼容性。)
同样,我已经要求过SerializationFormatter的安全许可。如果不实行恰当的保护,对于你的类来说,GetObjectData()可能存在安全漏洞。恶意代码可能会产生一个StreamingContext,从而可以用GetObjectData()方法从对象中取得值,或者不断修改版本而取得另一个SerializationInfo,或者重新组织修改的对象。这就许可了恶意的开发者来访问对象的内部状态,在流中修改它们,然而发送一个修改后的版本给你。对SerializationFormatter进行许可要求可以封闭这个安全漏洞。这样可以确保只有受信任的代码才能恰当的访问类的内部状态(参见原则47)。
但在使用ISerializable接口时有一个弊端,你可以看到,我很早就让MyType成为密封(sealed)的,这就强制让它只能成为叶子类(leaf
class)。在基类实现ISerializable接口就隐式的让所有派生类也序列化。实现ISerializable就意味关所有派生类必须创建受保护构造函数以及反序列化。另外,为了支持非密封类,你必须在GetObjectData()方法创建hook,从而让派生类可以添加它们自己的数据到流中。编译器不会捕获任何这样的错误,当从流中读取派生类时,因缺少恰当的构造构造函数会在运行时抛出异常。缺少hook的GetObjectData()方法也意味着从派生类来的数据不会保存到文件中。当然也不会有错误抛出。所以我要推荐:在叶类中实现Serializable。
我没有说这,因为它不工作:为了派生类的序列化,你的基类必须支持序列化。修改MyType
,让它成为了一个可序列化的基类,你要把序列化构造函数修改为protected,然后创建一个虚方法,这样派生类就可以重载它并存储它们的数据。
using System.Runtime.Serialization;
using System.Security.Permissions;
[Serializable]
public class MyType : ISerializable
{
private string _label;
[NonSerialized]
private int _value;
private OtherClass _object;
private const int DEFAULT_VALUE = 5;
private int _value2;
// public constructors elided.
// Protected constructor used only by the Serialization
framework.
protected MyType( SerializationInfo info,
StreamingContext cntxt )
{
_label = info.GetString( "_label" );
_object = ( OtherClass )info.GetValue( "_object", typeof
( OtherClass ));
try {
_value2 = info.GetInt32( "_value2" );
} catch ( SerializationException e )
{
// Found version 1.
_value2 = DEFAULT_VALUE;
}
}
[ SecurityPermissionAttribute( SecurityAction.Demand,
SerializationFormatter =true ) ]
void ISerializable.GetObjectData(
SerializationInfo inf,
StreamingContext cxt )
{
inf.AddValue( "_label", _label );
inf.AddValue( "_object", _object );
inf.AddValue( "_value2", _value2 );
WriteObjectData( inf, cxt );
}
// Overridden in derived classes to write
// derived class data:
protected virtual void
WriteObjectData(
SerializationInfo inf,
StreamingContext cxt )
{
}
}
一个派生类应该提供它自己的序列化构造函数,并且重载WriteObjectData方法:
public class DerivedType : MyType
{
private int _DerivedVal;
private DerivedType ( SerializationInfo info,
StreamingContext cntxt ) :
base( info, cntxt )
{
_DerivedVal = info.GetInt32( "_DerivedVal" );
}
protected override void WriteObjectData(
SerializationInfo inf,
StreamingContext cxt )
{
inf.AddValue( "_DerivedVal", _DerivedVal );
}
}
从流中写入和读取值的顺序必须保持一致。我相信先读写基类的数据应该简单一些,所以我就这样做了。如果你写的代码不对整个继承关系进行精确的顺序序列化,那么你的序列化代码是无效的。
.Net框架提供了一个简单的方法,也是标准的算法来支持对象的序列化。如果你的类型须要持久,你应该遵守这个标准的实现。如果你的类型不支持序列化,那化其它使用这个类的类也不能序列。为了让使用类的客户更加方便,尽可能的使用默认序列化特性,并且在默认的特性不满足时要实现ISerializable
接口。