系列三:C#表达式设计(序列化)
1.NET序列化会将对象中的所有成员变量保存到一个输出流中。但要记住,只有当对象中的所有类型都支持序列化时,这里谈的机制才会正常工作。
2.不打算序列化对象中的所有成员。在数据成员上添加 [NonSerialized]特性,可以告诉序列化框架不要将这些成员作为对象的状态来存储:
[Serializable]
public class MyType
{
private string _label;
[NonSerialized]
private int _cachedValue;
private OtherClass _object;
}
3.当使用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接口。
 
 
                     
                    
                 
                    
                
 
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号