• 深入挖掘.NET序列化机制——实现更易用的序列化方案


    深入挖掘.NET序列化机制——实现更易用的序列化方案

    .NET框架为程序员提供了“序列化和反序列化”这一有力的工具,使用它,我们能很容易的将内存中的对象图转化为字节流,并在需要的时候再将其恢复。这一技术的典型应用场景包括[1] :

    • 应用程序运行状态的持久化;
    • 在应用程序之间通过剪切板传送对象;
    • 创建对象复本,以隔离用户操作造成的影响;
    • 在网络间传送对象。

    然而,.NET框架提供的默认序列化行为也存在着有诸多限制,尤其是在版本控制方面——比如一个使用SerializableAttribute标记,而未实现ISerializable的类型,在通过重构修改了某个字段的名称后,再反序列化之前的序列化结果时就会失败。

    本文首先举例说明了.NET默认序列化方案的限制;然后描述了通过扩展.NET序列化框架而期望达到的目标——我们已经在实际开发中实现;接下来介绍了.NET序列化框架所提供的扩展点,最后详细说明了如何通过这些扩展点实现一个更易用的序列化方案。

    需要特别说明的两点是:

    1. 完整的.NET序列化框架是非常强大的,而我们的改进方案仍然处于这一框架之内。本文所说的”局限”只是指使用SerializableAttribute、ISerializable时的局限,并不是整个序列化框架的局限。如果你已经对ISurrogateSelector、ISerializationSurrogate、SerializationBinder等概念很熟悉了,我想你也许已经有了自己的解决方案。
    2. 为什么要在.NET序列化框架内进行扩展,而不是实现另外一套独立的解决方案呢?一来我们想充分利用框架提供的机制,比如对象图的访问与维护——这些逻辑实现起来应该会比较困难;再者可以充分利用已有的序列化代码,比如基础类库中的类型和第三方库中的类型——它们大都还是使用的.NET默认序列化机制,这样可以节省非常大的工作量。

    .NET默认序列化方案及其的限制

    在.NET中,为了使某个类型成为可序列化的,最简单的方式是使用SerializableAttribute属性,如下代码所示。

    复制代码
    [Serializable]
    class Person
    {
        private string name;
    
        private int agee;
    
        ...
    }
    
    var formatter = new BinaryFormatter();
    formatter.Serialize(someStream, aPerson);
    复制代码

    但在实际的开发项目中,如果真的这样做了,那么在产品的第一个版本发布后,就很有可能会面临下面的问题。

    • 你发现agee拼写错了,非常想改正它(嗯,我是完美主义者)!
    • 你发现Person这个名字太宽泛了,也许改为Employer更好。
    • 你想添加一个新字段,但又不想或不能在IDeserializationCallback中为新字段赋值——确实存在这种情况,请参考我的另一篇文章http://www.cnblogs.com/brucebi/archive/2013/04/01/2993968.html
    • 你想在Person的序列化过程中获得更多的控制,所以想改为实现ISerializable接口。

    所有这些都不能实现,因为它们所带来的代码修改都将造成软件不再兼容之前的版本。是的,我们总可以选择在最开始的时候就使用ISerializable接口(和一个反序列化构造函数),但如果你也有”懒惰“的美德,就会觉得这样做很不爽!

    此外,我们在开发过程中还遇到了如下几种情况:

    • 序列化第三方库中的对象,但这些类型没有标记为Serializable。
    • 有时我们需要把某个属性(property)序列化,而非相应的字段,这样反序列化时就可以通过属性的set方法执行一些额外的逻辑。
    • 某些对象的序列化过程效率很低,我们想提供一个更高效的实现。
    • 我们想实现一个”对象/存储映射“方案,以使我们能像”对象/关系映射“那样在内存与存储设备(包括数据库、文件系统等)间进行转储。我们的方案不像ORM那样复杂,但它更适合我们的产品,效率更高,自动化程度更高。

    而要解决这些问题,都要诉诸于对.NET序列化框架更深入的理解。

    我们能做到什么

    在最终的方案中我们将给出一个PersistedAttribute和一个IPersistable接口,它们与SerializableAttribute和ISerializable很类似,但能解决前面提到的问题。下面的代码说明了PersistedAttribute的使用方法及其功能。

    复制代码
    // 使用PersistedAttribute代替SerializableAttribute,以使用自定义的序
    // 列化机制进行处理。类的名字可以修改,只要保证Persisted的参数不变就
    // 可以。
    [Persisted("a unique identifier of the class")]
    class Person 
    {
        // 与SerializableAttribute不同,只有明确标记为Persisted的字段
        // 或属性才会被序列化。而且每个序列化项都可以指定一个名称,这
        // 样,当字段名称改变后,只要此项的名称不变,就能兼容之前的版
        // 本。比如,可以把name改为fullName,而无须做其它任何修改。
        [Persisted("Name")]
        private string name;
    
        [Persisted("Age")]
        private int age;
    
        // 对于新添加的字段,通过将其声明为“可选的”,反序列化过程就
        // 不会出错,之后可以在IDeserializationCallback中为其赋值,或
        // 者可以通过Value属性为其指定默认值。
        [Persisted("Id", Optional = true, Value = "any thing")]
        private string id;
    
        // 对于新添加的可选项,也可以为其指定一个计算函数,这样系统在
        // 反序列化时如果发现流中没有存储相应的值,就会调用此函数来为
        // 其计算相应的值。
        [Persisted("Gender", Optional = true, Calculator = CalculateGenderById)]
        private Gender gender;
    
        // 属性也可以被序列化。在序列化时,系统将保存get方法的结果,而
        // 反序列化时,则会通过set方法设置属性的值,此方法中的所有逻辑
        // 都将会执行。
        [Persisted("SomeProperty")]
        public int SomeProperty
        {
            get
            {
                ...
            }
    
            set
            {
                ...
            }
        }
    }
    复制代码

     IPersistable接口则可以与PersistedAttribute进行各种组合、替换,如下面的代码所示。

    复制代码
    public interface IPersistable
    {
        // 在序列化时获取对象的数据。
        void GetObjectData(SerializationInfo info);
    
        // 在反序列化时设置对象的数据。
        void SetObjectData(SerializationInfo info);
    }
    
    // 与ISerializable一样,实现IPersistable接口也要求有Persisted标记。
    [Persisted("a unique identifier of the class")]
    class Person : IPersistable
    {
        // Persisted标记与IPersistable接口可以共存,系统会先处理
        // 被标记的字段或属性,然后调用IPersistable接口的成员。
        [Persisted("Name")]
        private string name;
    
        // 可以去掉之前使用的Persisted标记,然后在IPersistable
        // 接口中以同样的名称进行读取或设置。
        // [Persisted("Age")]
        private int age;
    
        private Gender gender;
    
        void GetObjectData(SerializationInfo info)
        {
            info.SetValue("Age", age);
    
            // 保存额外的数据。
            info.SetValue("G", gender);
        }
    
        void SetObjectData(SerializationInfo info)
        {
            // 读取之前使用标记序列化的内容。
            age = info.GetInt32("Age");
    
            try
            {
                // 处理版本兼容问题。
                gender = (Gender)info.GetValue("G");
            }
            catch (Exception e)
            {
            }
        }
    }
    复制代码

    此外,新的方案还提供了IPersistor接口,通过它可以为任何类型提供自定义的序列化代码,不管这个类型是不是可序列化的。

    复制代码
    // 可以为其它类型的对象提供序列化功能的接口。
    public interface IPersistor
    {
        void GetObjectData(object obj, SerializationInfo info);
        void SetObjectData(object obj, SerializationInfo info);
    }
    
    // 为List<int>类型的对象提供自定义的序列化代码,虽然List<int>本身已经是可序列化的.
    public class IntListPersistor : IPersistor
    {
        void GetObjectData(object obj, SerializationInfo info)
        {
            var list = (List<int>)obj;
            // some more efficient codes.
            ... ...
            info.SetValue(...);
        }
    
        void SetObjectData(object obj, SerializationInfo info)
        {
            var list = (List<int>)obj;
            // some more efficient codes.
            ... ...
            someValue = info.GetValue(...);
        }
    }
    
    // 可以为其它非可序列化类型提供自定义的序列化代码。
    public class RelativePersistor : IPersistor
    {
        void GetObjectData(object obj, SerializationInfo info)
        {
            var target = (Some3rdPartyNonSerializableClass)obj;
            ... ...
        }
    
        void SetObjectData(object obj, SerializationInfo info)
        {
            var target = (Some3rdPartyNonSerializableClass)obj;
            ... ...
        }
    }
    
    // 需要在程序开始的时候将实现类注册到系统中。
    PersistManager.Register(typeof(List<int>), new IntListPersistor());
    PersistManager.Register(typeof(Some3rdPartyNonSerializableClass, new RelativePersistor());
    复制代码

    下面,我们将介绍相应技术的实现思路。

    如何切入.NET的序列化框架

    首先要解决的问题是:如何将自定义的序列化机制插入到.NET序列化框架中。我假设你已经知道如何使用BinaryFormatter或者SoapFormatter,而在此我想简单的描述一下formatter的一些行为细节。后文中我们将以BinaryFormatter为例。

    BinaryFormatter上有一个SurrogateSelector属性(surrogate是“代理”的意思,surrogate selector便是代理选择器),它的类型如下代码所示:

    复制代码
    public interface ISurrogateSelector 
    {
       void ChainSelector(ISurrogateSelector selector);
    
       ISurrogateSelector GetNextSelector();
    
       ISerializationSurrogate GetSurrogate(Type type,
          StreamingContext context, out ISurrogateSelector selector);
    }
    复制代码

    其中用到的ISerializationSurrogate(serialization surrogate便是序列化代理喽)的定义如下:

    复制代码
    public interface ISerializationSurrogate
    {
       void GetObjectData(Object obj,
          SerializationInfo info, StreamingContext context);
    
       Object SetObjectData(Object obj,
          SerializationInfo info, StreamingContext context,
          ISurrogateSelector selector);
    }
    复制代码

    当BinaryFormatter在序列化一个对象obj时,它会检查自己的SurrogateSelector属性是否非空,如果非空,便会以obj的类型为参数调用其GetSurrogate方法,如果此方法返回一个有效的对象surrogate(ISerializationSurrogate),则formatter会调用surrogate.GetObjectData(obj, ...),这时surrogate对象便获得机会来执行自定义的逻辑了。

    对,这就是奇迹发生的地方!

    我们要做的就是实现自定义的ISurrogateSelector和ISerializationSurrogate类,在合适的时候调用自定义的代码。然后,在使用时将其注入到BinaryFormatter中,如下面代码所示。

    var formatter = new BinaryFormatter(
            new TheSurrogateSelector,  
            new StreamingContext(StreamingContextStates.All));
    formatter.Serialize(stream, objectGraph);

    实现更易用的序列化方案

     先来看ISurrogateSelector的实现(为简洁起见,去掉了很多优化相关的代码)。

    复制代码
    public class PersistSurrogateSelector : SurrogateSelector
    {
        private readonly PersistSurrogate inner = new PersistSurrogate();
    
        private readonly ISerializationSurrogate surrogate;
    
        public PersistSurrogateSelector()
        {
            surrogate = FormatterServices.GetSurrogateForCyclicalReference(inner);
        }
    
        public override ISerializationSurrogate GetSurrogate(Type type,
            StreamingContext context, out ISurrogateSelector selector)
        {
       
            return inner.CanHandle(type) ? surrogate
                : base.GetSurrogate(type, context, out selector);
        }
    }
    复制代码

    其中的PersistSurrogate即是我们自定义的序列化代理,其代码如下所示:

    复制代码
    public class PersistSurrogate : ISerializationSurrogate
    {
        public void GetObjectData(object obj, SerializationInfo info, 
            StreamingContext context)
        {
            // 使用注册的IPersistor实现来对对象进行序列化和反序列化。
            var manager = PersistorManager.Instance;
            var persistor = manager.GetPersistor(obj.GetType());
            persistor.GetObjectData(obj, info);
        }
    
        public object SetObjectData(object obj, SerializationInfo info, 
            StreamingContext context, ISurrogateSelector selector)
        {
            var manager = PersistorManager.Instance;
            var persistor = manager.GetPersistor(obj.GetType());
            return persistor.SetObjectData(obj, info);
        }
    
        internal bool CanHandle(Type type)
        {
            // 如果已经为类型注册了IPersistor接口实现,则自定义机制可以处理。
            return PersistorManager.Instance.IsPersistable(type);
        }
    }
    复制代码

    代码中的PersistorManager就是管理自定义的IPersistor接口实现与相应的类型对应的管理类(前面的代码中提到过),一会儿我们会提到PersistorManager.Instance.IsPersistable的实现,至于此类的其它功能则不再赘述。

    这样我们就实现了对任意类型进行自定义序列化的功能,下面简要总结一下:

    1. 为要序列化的类型T定义一个IPersistor接口的实现P;
    2. 将T与P注册到PersistorManager中;
    3. 在创建BinaryFormatter时,将PersistSurrogateSelector对象传入;

    有了这个基础,再实现IPersistable和PersistedAttribute功能就比较简单了,来看PersistorManager::IsPersistable方法的实现:

    复制代码
    public bool IsPersistable(Type type)
    {
        Utility.CheckNotNull(type);
        var at = typeof(PersistedAttribute);
        return Attribute.GetCustomAttribute(type, at, false) != null
            || exactMatches.ContainsKey(type)
            || derivedMatches.GetValue(type) != null
            || dynamicMatches.Any(i => i.CanHandle(type));
    }
    复制代码

    其中的Attribute.GetCustormAttribute即是在判断具体的类型上是否有PersistedAttribute标记,如果有我们就可以为其合成一个IPersistor接口的实现——这样,使用PersistedAttribute标记的类型,则不再需要显示的注册。代码中的exactMatches,derivedMatches和dynamicMatches分别用于处理那些能够为某个具体的类型提供序列化功能,能够为一个派生体系提供序列化功能及能够动态决定是否可以提供序列化功能的IPersistor实现。

    我们可以定义一个Persistor实现来统一处理那些有PersistedAttribute标记的类型,在此之前先来看PersistorManager::GetPersistor的定义:

    复制代码
    public IPersistor GetPersistor(Type mapped)
    {
        Utility.CheckNotNull(mapped);
        if (exactMatches.ContainsKey(mapped))
        {
            return exactMatches[mapped];
        }
    
        var derived = derivedMatches.GetValue(mapped);
        if (derived != null)
        {
            exactMatches[mapped] = derived;
            return derived;
        }
    
        var dynamic = dynamicMatches.FirstOrDefault(i => i.CanHandle(mapped));
        if (dynamic != null)
        {
            exactMatches[mapped] = dynamic;
            return dynamic;
        }
    
        var auto = new Persistor(mapped);
        exactMatches[mapped] = auto;
        return auto;
    }
    复制代码

    而Persistor的实现逻辑如下:

    1. 提取mapped上所有有PersistedAttributed标记的成员;
    2. 在序列化时(IPersistor::GetObjectData中),迭代所有的标记项,用标记的名称和具体的字段值调用info.AddValue;
    3. 在反序列化时(IPersistor::SetObjectData中),迭代所有标记项,用标记的名称从SerializationInfo中取值,并作如下处理:
      1. 如果取值成功,则将其设置给要反序列化的对象;
      2. 如果获取失败,则
        1. 如果此项未标记为Optional,则抛出异常,否则:
        2. 如果此项有Value或Calculator设置,则通过它们为当前字段赋值,否则保留字段的默认值(default(T))。

    至于具体实现代码,我们这里就省略了。 

    省略掉的部分

    读到这里,相信大家对.NET的序列化框架的扩展就有所感受了。不过我们还没有介绍如何处理类型名称改变的问题,这里只给出一个引子——使用BinaryFormatter的Binder属性和自定义的SerializationBinder派生类——更多的细节相信大家都能搞定的。

    其实.NET序列化机制还有很多可以挖掘的地方,比如IObjectReference,每一个看似简单的接口都能给我们无限发挥的空间。

    好了,就到这里吧。欢迎大家来探讨。


    参考资料:

    [1] http://msdn.microsoft.com/en-us/magazine/cc301761.aspx

     
     
    分类: .NET
  • 相关阅读:
    搭建自己的博客(四):优化首页和详情页
    搭建自己的博客(三):简单搭建首页和详情页
    搭建自己的博客(二):创建表,创建超级用户
    搭建自己的博客(一):前期准备
    linux系列(五):rm命令
    linux系列(四):mkdir命令
    linux系列(三):pwd命令
    Statically Linking freeglut
    ffmpeg 版本升级到 4.0 增加 libaom 库 [AOMedia 的 AV1 视频编码格式]
    FFmpeg configure: rename cuda to ffnvcodec 2018-03-06
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3179981.html
Copyright © 2020-2023  润新知