Effective C# 尽可能将类型实现为可序列化的类型

持久化(persistence)是类型的一个核心特性。这种特性往往是在我们忽略支持它们的时候,才会被注意到。如果我们的类型没有正确地支持序列化(serialization),那么其他开发人员在使用我们的类型作为成员或者基类的时候将有许多工作要做。他们必须自己实现这样的标准特性。如果不访问类型的私有实现细节,要想为类型正确实现序列化几乎是不可能的。如果我们自己不支持序列化,让类的用户来添加序列化支持将会很困难,甚至根本做不到。

因此,只要有实际意义,我们都应该尽可能地为我们的类型添加序列化支持。只要类型表示的不是UI控件、窗口或者表单,支持序列化都是有意义的。我们不能因为序列化支持需要额外的工作而选择不支持它们。. NET的序列化支持非常简单,我们没有任何理由不支持它们。许多情况下,只要添加一个Serializable特性就足够了,例如:

 

[Serializable]

public class MyType

{

private string _label;

private int _value;

}

 

之所以可以像上面那样做,是因为MyType的所有成员都是可序列化的:string和int都支持序列化。为什么我们要尽可能地为类型添加序列化支持呢?下面的代码可以帮助我们明白其中的缘由:

 

[Serializable]

public class MyType

{

private string      _label;

private int        _value;

private OtherClass _object;

}

 

字段_object的类型为 OtherClass,只有在OtherClass类型支持序列化的前提下,应用于MyType上的Serializable特性才能正常工作。如果 OtherClass不支持序列化,那么我们将在序列化MyType时得到一个运行时错误。为了支持序列化MyType,我们必须自己在MyType中编写代码,对OtherClass对象进行序列化。如果不了解OtherClass类型中定义的内部情况,要想实现这一点是不可能的。

.NET序列化会将对象中的所有成员变量保存到一个输出流中。而且,它还支持任意的对象图:即使对象中有循环引用,Serialize()和Deserialize()方法也会正确地对每个对象仅做一次存储和恢复。当一些交织成网状的对象被反序列化时,.NET序列化框架会正确地重建对象间的引用关系。最后需要指出的是,Serializable特性同时支持二进制序列化和SOAP序列化。本条款中的所有技巧也都支持这两种序列化格式。但要记住,只有当对象图中的所有类型都支持序列化时,这里谈的机制才会正常工作。这也是我们强调所有的类型都要支持序列化的原因。只要漏掉了一个类,就很难对整个对象图进行序列化。过不了多久,每个人都要再次自己编写序列化代码。

添加Serializable特性是一种最简单的支持对象序列化的技巧。但是最简单的方案并不总是正确的。有时候,我们可能并不打算序列化对象中的所有成员:有些成员可能只是为了缓存一个耗时较长的操作的结果。其他一些成员可能会持有一些只有活动内存中(in-memory)的操作才需要的运行时资源。我们也可以使用特性管理这些需求。在数据成员上添加 [NonSerialized]特性,可以告诉序列化框架不要将这些成员作为对象的状态来存储:

 

[Serializable]

public class MyType

{

private string _label;


[NonSerialized]

private int _cachedValue;

private OtherClass _object;

}

 

NonSerialized 成员会为类的设计者增加一些额外的负担。在反序列化的过程中,.NET框架的序列化API不会初始化那些NonSerialized成员。因为类型的构造器不会被调用,所以成员的初始化器也就不会被执行。当使用Serializable特性时,那些NonSerialized成员只会得到系统设定的默认值:0或者null。如果这种默认的0初始化机制不合适,我们就需要实现IDeserializationCallback接口来初始化它们。 IDeserializationCallback仅包含一个方法OnDeserialization()。在整个对象图被反序列化之后,.NET框架会调用该方法。我们应该使用该方法来初始化对象中包含的NonSerialized成员。由于整个对象图已经被读入内存中,因此在对象或者它的任何 Serializable成员上调用函数都是安全的。不幸的是,也有不安全的地方。在整个对象图被读入内存中之后,.NET框架会在对象图中每一个支持 IDeserializationCallback接口的对象上调用OnDeserialization()方法。在处理 OnDeserialization()方法的过程中,对象图中的任何其他对象都可能访问我们对象上的公有成员。如果它们先被调用,我们对象中的 NonSerialized成员就为0或者null。由于.NET框架并不保证调用的顺序,因此我们必须确保我们的所有公有方法都能处理 “NonSerialized成员未被初始化”的情况。

现在大家已经理解了为什么要尽可能地为所有类型添加序列化支持:NonSerialized类型在被用于其他需要序列化的类型中时,会导致更多的工作。大家也掌握了如何使用特性来支持最简单的序列化方法,包括如何初始化NonSerialized成员。

另外,序列化数据也需要支持不同版本的程序。为类型添加序列化支持意味着我们在未来可能需要读取以前版本的序列化数据。当发现对象图中增加或者删除了某些字段,使用Serializable特性所产生的代码将会抛出异常。如果我们准备支持多个版本,并且需要对序列化过程做更多的控制,则应该使用 ISerializable接口。该接口定义了一些挂钩(hook)函数,允许我们对类型的序列化过程进行定制。ISerializable接口使用的方法和存储(storage)与默认初始化机制使用的方法和存储相同。这意味着我们刚开始创建类的时候,可以使用Serializable特性,如果有必要提供扩展时,再为ISerializable接口添加支持。

作为示例,下面我们考虑一下在MyType的第2个版本添加了一个字段之后,如何为其提供序列化支持。简单地添加一个新字段所产生的格式显然与之前磁盘上存储的版本不一致。

 

[Serializable]

public class MyType

{

private string _label;

[NonSerialized]

private int _value;

private OtherClass _object;

// 下面的字段在版本2中加入。

// 当发现版本1.0的文件中没有该字段时,运行时会抛出异常。

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;

// 忽略其他公有构造器。

// 仅由序列化框架使用的私有构造器。

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 )

    
{

      
// 发现是版本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 );

}


}

 

序列化流将每一个数据项存储为一个键/值(key/value)对。由特性产生的代码会使用变量的名字作为变量值的键。添加ISerializable接口时,键名与变量的顺序必须匹配。变量的顺序即在类中的声明顺序。(顺便提一句,这就意味着重新调整类中变量的顺序或者重新命名变量,将会打破先前序列化创建的文件的兼容性。)

另外,上面的代码还要求 SerializationFormatter异常安全许可。如果没有被正确地保护,GetObjectData()可能会为我们的类引入安全漏洞。恶意代码可能会创建一个StreamingContext,然后使用GetObjectData()获取对象的值,再将更改后的版本序列化到另一个 SerializationInfo中,从而实现对象状态的更改。这就使得恶意的开发人员可以访问对象的内部状态,然后在流中更改,最后再将更改结果传回对象。要求SerializationFormatter许可则堵住了这个潜在的漏洞。这确保了只有受信任的代码才能访问该函数,获取对象的内部状态。(有关代码安全,参见条款47。)

实现 ISerializable接口也会带来一项弊端。大家可能已经发现上面的MyType被实现为sealed类型。这使得MyType只能成为一个叶子类。在基类中实现ISerializable接口将使得所有派生类的序列化变得更为复杂。这意味着每一个派生类都必须为反序列化创建受保护的构造器。另外,为了支持非密封(nonsealed)的类,我们需要在GetObjectData()方法中创建挂钩(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;

// 忽略其他公有构造器。

// 仅由序列化框架使用的私有构造器。

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 )

    
{

      
// 发现是版本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 );

}


// 在派生类中重写以序列化派生类中的数据:

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框架为对象序列化提供了一个简单、标准的算法。如果我们的类型需要持久化,那就应该遵循标准的实现。如果我们不为类型添加序列化支持,那么其他使用我们类型的类也不能支持序列化。我们所做的工作应该尽可能地使类的使用者更加方便。如果可以,应该使用默认的方式来支持序列化;如果默认的Serializable特性不行,则应该实现 ISerializable接口。

posted @ 2008-10-30 10:16  瞪着你的小狗  阅读(833)  评论(0编辑  收藏  举报