Item25 选择可序列化类型

  持久性是类型的一个核心属性,这是一个很容易被忽略的基本元素。如果你的类型不支持序列化,那么你就会为使用你的类型作为成员或基类的开发者增加了很多工作量,他们必须在不能访问你的类型私有细节的情况下对你的类型实现序列化,很明显,如果你不在定义类型时将类型声明为可序列化,那么使用你类型的客户端是很难或根本不可能为你的类型添加这个属性的。

  为你定义的类型添加序列化,对于那些不用承载UI元素,窗口或表单的类型来说是有实际意义的,感觉有额外的工作是没有理由的,在.net中支持序列化是非常简单的,只用在类型定义时添加一个[Serializable]就可以了,那么你还有什么理由不让你的类型支持序列化呢?例如:

[Serializable]
    
public class MyType
    {
        
private string _lable;
        
private int _value;
    }
上例使用了[Serializable],从而MyType类型可被序列化,但是在我们对自定义类型使用[Serializable]Attribute时,我们必须保证类型内部的成员也支持序(string和int类型都支持.net序列化),这就是为什么我们要尽可能的让自定义类型支持序列化。例如:
[Serializable]
    
public class MyType
    {
        
private string _lable;
        
private int _value;
        
private OtherClass _object;
    }
只有在OtherClass类型支持.net序列化的情况下,上述Serializable才能正常工作,否则将出现运行时错误,那么使用该类型的客户端(也就是现在的MyType类)必须自己写代码并且在不知道OtherClass类型内部定义的情况下来序列化MyType和OtherClass。
  .net序列化会将对象的所有成员都保存在序列化的流中。并且.net序列化代码支持所有对象图,甚至是对象中包含循环引用。这也证明了上面的观点,尽可能的让你的类型支持序列化。.net的序列化和反序列化是通过Serialize和Deserialize方法来完成的,这两个方法分别只将对象序列化到流中和重新存储到实际对象中各一次。
如果你序列化的对象经过层层包含形成一个对象网,那么在反序列化的时候.net序列化框架会为你返回原来的对象网,也就是当对象图重新创建时,.net会为你正确的重新存储原来的相关对象形成的对象网。这样一个SerializableAttribute支持两种序列化方式:二进制和SOAP。
  有时候我们不想序列化对象的某些成员怎么办呢,比如有些对象成员的存在就是用来缓存某些操作的结果,还有一些成员是用来存储仅仅在内存操作是需要的运行时资源,那么这些成员确实没有序列化的必要,那么我们可以使用[NonSerializable]来将这些不需要存储为对象状态的字段标识为不可序列化。
[Serializable]
    
public class MyType
    {
        
private string _lable;
        
        [NonSerialized]
        
private int _value;
    }
.net序列化API函数在反序列化是不会初始化被标识为[NonSerialized]的成员的,而且不会有类型的构造函数被调用,于是这些成员的初始化将不会被执行,当你使用SerializableAttribute时,在执行反序列化的时候这些成员只会被系统初始化为0或null。如果系统默认的零值不正确或不符合要求是,你可以实现IDeserializableCallback接口,该接口包含一个方法:OnDeserialization.  .net序列化框架会在整个对象图被反序列化之后调用这个方法。所以当执行OnDeserialization的时候可能同时存在其它对象访问该公共成员的情况,于是此时使用还是系统的默认零值,所以在方法中要处理这种被标识为NonSerialized的成员为0或null的情况。
  序列化的数据有时会面临着版本升级的问题,当你为你的类型添加序列化支持时,就意味着你将来有可能读取旧版本的这个对象,当SerializableAttribute产生的代码发现需要反序列化的对象图中的对象添加了或者移除了某些字段,那么它将抛出一个异常。如果你定义的类型需要支持序列化并且会面临版本升级,那么你就需要对序列化过程进行更多的控制,这时就需要用到ISerializable接口了,这个接口提供了一个名为GetObjectData的方法用于自定义序列化的行为,实际上这个接口使用的方法和用于存储数据的数据结构和和系统默认的SerializableAttribute使用的方法和数据结构是一样的,这就意味着你应该为任何你创建的类型使用SerializableAttribute,当这个类型需要自己在序列化逻辑方面的扩展时,便可实现ISerializable接口。
  现在假设上面例子中的MyType是1.0版本的,现在定义MyType的升级版2.0,添加了一个字段:
[Serializable]
    
public class MyType
    {
        
private string _lable;
        
        [NonSerialized]
        
private int _value;

        
//2.0版本添加的字段,当运行时发现硬盘上的1.0版本的该类型不存在该字段便会抛出异常。
        private int _value2;
    }
很明显,当在上例中从硬盘读取流来反序列化为对象时,运行时会抛出异常,这时就需要类型实现ISerializable接口来自定义序列化行为。当你的类型实现ISerializable接口(定义了一个方法GetObjectData)时,不仅要添加GetObjectData方法的实现用于序列化对象,还要提供一个用于将流反序列化为对象的构造函数。看看如何解决刚才的问题:
using System.Runtime.Serialization;
using System.Security.Permissions;

[Serializable]
    
public sealed class MyType:ISerializable
    {
        
private const int DEFAULTVALUE = 10;
        
        
private string _lable;
        
        [NonSerialized]
        
private int _value;

        
private int _value2;

        
private MyType(SerializationInfo info,StreamingContext cntxt)
        {
            _lable 
= info.GetString("_label");
            _value 
= info.GetInt32("_value");
            
try
            {
                _value2 
= info.GetInt32("_value2");
            }
            
catch(SerializationException e)
            {
                _value2 
= DEFAULTVALUE;
            }
        }

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter 
= true)]
        
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext cntxt)
        {
            info.AddValue(
"_label", _lable);
            info.AddValue(
"_value", _value);
            info.AddValue(
"_value2", _value2);
        }
    }
从上例中可以看出,在序列化的流中是按健/值对的形式来存储对象的数据的,有一点需要特别注意,就是当你在序列化和反序列化时对字段操作的顺序必须和类里面定义字段的顺序一致,并且在序列化和反序列化的两个方法中的键必须匹配。这就意味着改变类型定义字段的顺序和名称可能会破坏和已经创建的文件的兼容性。
  大家可能还会注意到我为GetObjectData方法添加的[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)],这是一个安全许可。不知道大家有没有注意到GetObjectData可能会成为你的类型的一个安全隐患。恶意代码可以创建一个StreamingContext类,然后通过GetObjectData方法读取类型的数据,然后对其进行修改,接着创建另一个SerializationInfo,最后重新创建一个对象,然后返回给你,这时你得到的是恶意开发者修改数据后的对象,想想是件多么危险的事。添加SerializationFormatter可以隐藏这个潜在的隐患,它保证了只有可信任的代码能通过这个GetObjectData访问对象内部数据。
  实现ISerializable接口确实非常有用,但是它也有一个缺点,必须将类型定义为Sealed(就像上例中MyType一样),如果不定义为sealed,那么默认的该类的所有派生类都将实现ISerializable接口,都将可序列化,于是在每个派生类中都要添加用于反序列化的构造函数,并且还要在基类中创建虚函数,以便在子类中覆写来实现子类中新增数据的序列化操作,如果你没有提供这两个方法,编译器是不会报错的。如果缺少用于反序列化的构造函数,运行时将会在你从流中反序列化时抛出异常,如果缺少辅助GetObjectData的虚方法,那么子类新增的字段将永远不会存储到文件,而且不会有任何错误产生(编译,运行时),这是件多么危险的事情啊。
  如果由于子类需要支持序列化,那么子类就必须可序列化,这时需要将基类中用于反序列化的构造函数定义为protected,并且定义一个虚方法,用于在子类中覆写来实现子类中新增数据的序列化操作,说了这么多,看个例子吧:
[Serializable]
    
public class MyType:ISerializable
    {
        
private const int DEFAULTVALUE = 10;
        
        
private string _lable;
        
        [NonSerialized]
        
private int _value;

        
private int _value2;

        
protected MyType(SerializationInfo info,StreamingContext cntxt)
        {
            _lable 
= info.GetString("_label");
            _value 
= info.GetInt32("_value");
            
try
            {
                _value2 
= info.GetInt32("_value2");
            }
            
catch(SerializationException e)
            {
                _value2 
= DEFAULTVALUE;
            }
        }

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter 
= true)]
        
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext cntxt)
        {
            info.AddValue(
"_label", _lable);
            info.AddValue(
"_value", _value);
            info.AddValue(
"_value2", _value2);

            WriteObjectData(info, cntxt);
        }

        
protected virtual void WriteObjectData(SerializationInfo info, StreamingContext cntxt)
        {
 
        }
    }
    
public class MyDerivedType : MyType
    {
        
private int _derivedValue;

        
private MyDerivedType(SerializationInfo info, StreamingContext cntxt):base(info,cntxt)
        {
            _derivedValue 
= info.GetInt32("_derivedValue");
        }

        
protected override void WriteObjectData(SerializationInfo info, StreamingContext cntxt)
        {
            info.AddValue(
"_derivedValue", _derivedValue);
        }
    }
如上例,我们在基类中将用于反序列化的构造函数MyType(SerializationInfo info,StreamingContext cntxt)定义为protected,并添加了一个虚函数WriteObjectData,在子类中覆写用于序列化子类新增数据。如同在要遵循类中字段的定义顺序,要先序列化和反序列化基类中的字段,然后是子类中的字段,如果不按照继承体系的顺序,那么序列化代码将不会工作。
  总结:尽量要定义可序列化的类,这会为你的客户端减少很多工作,尽量使用.net提供的Serializable,如果要自定义序列化逻辑,那么请实现ISerializable接口。
posted @ 2009-09-14 23:18  PeterLau  阅读(335)  评论(0)    收藏  举报