Take a look at GW

【C#】详解C#序列化

目录结构:

contents structure [+]

 

 

在这边文章中,笔者将会将会详细阐述C#中的序列化和反序列,希望可以对你有所帮助。

1.简介

众所周知,序列化是将对象或对象图转化字节流的过程,反序列化是将字节流转化为对象图的过程。
如果要使一个类型可序列化的话,必需向类型应用定制特性System.SerializableAttribute。请注意,SerializableAttribute特性只能应用于引用类型(class)、值类型(struct)、枚举类型(enum)和委托类型(delegate)。枚举和委托类型总是可序列化的所以不必显示使用SerializableAttribute特性。

序列化必须要使用到序列化器,它用于完成将数据转化为特定格式的数据。以下列举四种格式化器:
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 以二进制的格式对对象进行序列化和反序列操作。

System.Runtime.Serialization.Formatters.Soap.SoapFormatter 以SOAP的格式对对象进行序列化和反序列化操作,从.NET Framework2.0开始,该类就废弃,推荐使用BinaryFormatter类。

System.Runtime.Serialization.NetDataContractSerializer 用.NET Framework提供的类型,将类型实例序列化和返序列化为XML流或文档结构。

System.Runtime.Serialization.DataContractSerializer 使用指定的数据协定,将类型实例序列化和反序列化XML流或文档结构

System.Xml.Serialization.XmlSerializer 将类型的实例序列化和反序列化XML文档,该类允许控制如何将对象编码为XML文档。

2.控制序列化和反序列化

如果将SerializableAttribute特性应用于某个类型,那么标志该类型的实例可以进行序列化和反序列化操作,该类型实例的所有数据都可以进行序列化和反序列化操作,如果需要更精准的控制序列化和反序列化的数据,那么就需要控制序列化和反序列化的过程了。

这里笔者把序列化和反序列化的操作方式分为两种,分别为通过特性和通过接口的方式。

2.1 特性(OnSerializing、OnSerialized、OnDeserializing、OnDeserialized、NonSerialized...)

在这里笔者将会介绍序列化中常用的特性,用这些特性可以控制序列化的过程。

System.Runtime.Serialization.OnSerializingAttribute:
应用OnSerializingAttribute特性的方法,将会在序列化期间被调用。同时,应用OnSerializingAttribute特性的方法必须包含一个System.Runtime.Serialization.StreamingContext参数。

System.Runtime.Serialization.OnSerializedAttribute:
应用OnSerializedAttribute特性的方法,将会在序列化之后被调用。同时,应用OnSerializedAttribute特性的方法必须包含一个System.Runtime.Serialization.StreamingContext参数。

System.Runtime.Serialization.OnDeserializingAttribute:
应用OnDeserializingAttribute特性的方法,将会在被序列化期间被调用。同时,应用OnDeserializingAttribute特性的方法必须包含一个System.Runtime.Serialization.StreamingContext参数。

System.Runtime.Serialization.OnDeserializedAttribute:
应用OnDeserializedAttribute特性的方法,将会在被序列化之后调用。同时,应用OnDeserializedAttribute特性的方法必须包含一个System.Runtime.Serialization.StreamingContext参数。

System.NonSerializedAttribute:
应用NonSerializedAttribute特性的字段,将会不会序列化。可以利用这个特性保护保护敏感数据,NonSerializedAttribute不仅可以引用字段,还以应用于event。

 

例如:

[field:NonSerializedAttribute()]
public event ChangedEventHandler Changed;

下面给上面使用上面特性的类:

//标记为可序列化
    [Serializable]
    class MyType {
        Int32 x, y;
        //标记num为不可序列
        [NonSerialized]
        Int32 num;

        public MyType(Int32 x, Int32 y) {
            this.x = x;
            this.y = y;
            this.num=(x+y);
        }

        //标记该方法在序列化期间被调用
        [OnSerializing]
        private void OnSerializing(StreamingContext context) {
            //举例:在序列化前,修改任何需要修改的状态
        }

        //标记该方法在序列化之后被调用
        [OnSerialized]
        private void OnSerialized(StreamingContext context) {
            //举例:在序列化之后,恢复任何需要恢复的状态
        }

        //标记该方法在反序列化期间被调用
        [OnDeserializing]
        private void OnDeserialing(StreamingContext context) {
            //举例:在反序列化期间,为字段设置默认值
        }

        //标记该方法在反序列化之后被调用
        [OnDeserialized]
        private void OnDeserialized(StreamingContext context) {
            //举例:根据字段值初始化瞬间状态(比如num值)
            num = x + y;
        }
    }

2.2 接口(ISerializable)

在前面已经介绍过通过OnSerializing,OnSerialized,OnDeserializing,OnDeserialized等特性。除了使用特性,还可以让类型实现System.Runtime.Serialization.ISerializable接口。
该接口的定义如下:

public interface ISerializable{
    void GetObjectData(SerializationInfo info,StreamingContext context);
}

实现ISerializable接口,除了需要实现GetObjectData方法,还应该提供一个特殊的构造器。

注意:
ISerializable接口和特殊构造器旨在由格式化器使用,但其他代码可能调用GetObjectData来返回敏感数据,或传入损坏的数据。建议向GetObjectData方法和特殊构造器应用以下特性:
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter=true)]

如果在类型中必须访问提取对象中的成员,建议类型提供一个OnDeserialized特性或是实现IDeseializationCallback接口的OnDeserialization方法。调用该方法时,所有对象的字段都已经初始化好了。

    class Program
    {
        static void Main(string[] args)
        {
            MyItemType myItermType = new MyItemType("hello");
            using(MemoryStream memoryStream = new MemoryStream()){
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(memoryStream, myItermType);

                memoryStream.Position = 0;
                myItermType = null;

                myItermType = (MyItemType)binaryFormatter.Deserialize(memoryStream);
                Console.WriteLine(myItermType.MyProperty);//hello
            }

            Console.ReadLine();
        }
    }
    [Serializable]
    public class MyItemType : ISerializable,IDeserializationCallback
    {
        private string myProperty_value;
        [NonSerialized]
        private SerializationInfo m_info = null;

        public MyItemType(String property)
        {
            this.myProperty_value = property;
        }

        public string MyProperty
        {
            get { return myProperty_value; }
            set { myProperty_value = value; }
        }

        //在序列化期间被调用
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("props", myProperty_value, typeof(string));
        }

        //在反序列化期间被调用
        public MyItemType(SerializationInfo info, StreamingContext context)
        {
            //将SerializationInfo的引用保留下来。
            //之所以不在构造方法中完成字段赋值,是因为如果要访问当前对象的成员(方法),那么此时成员很有可能没有初始化完成,可能出现不可预期的结果
            m_info = info;
        }

        //在反序列化之后调用
        public void OnDeserialization(object sender)
        {
            myProperty_value = (string)m_info.GetValue("props", typeof(string));
        }
    }

在这里,在上面我们知道了SerializationInfo对象其中一个重要的方法就是AddValue,使用该方法可以将对象添加到序列化的过程中。SerializationInfo除了AddValue,还有一个值得说明的方法就是setType,使用这个方法可以设置序列化的数据类型,如果恰好该类型实现了IObjectReference接口的话,将会在反序列化之后,自动调用其抽象方法:
IObjectReference接口原型为:

public interface IObjectReference{
    Object GetRealObject(StreamingContext context);
}

看如下如何序列化和反序列化单实例的栗子:

    [Serializable]
    public sealed class Singleton : ISerializable {

        //该类型的实例
        private static readonly Singleton s_theOneObject = new Singleton();

        //实例字段
        public String Name = "Jeff";
        public DateTime Date = DateTime.Now;

        //私有构造器,只允许这个类型构造单实例
        private Singleton() { }

        //返回对该单实例的引用
        public static Singleton GetSingleton() {
            return s_theOneObject;
        }

        //序列化一个Singleton时调用
        [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
        void ISerializable.GetObjectData(SerializationInfo info,StreamingContext context) {
            info.SetType(typeof(SingletonSerializationHelper));
        }

        [Serializable]
        private sealed class SingletonSerializationHelper : IObjectReference {
            //该方法在对象反序列化之后调用
            public Object GetRealObject(StreamingContext context) {
                return Singleton.GetSingleton();
            }
        }

        //注意:特殊构造器不需要,因为它永远都不会被调用
    }

测试代码为:

        static void Main(string[] args)
        {
            Singleton[] a1 = { Singleton.GetSingleton(),Singleton.GetSingleton()};

            Console.WriteLine(a1[0]==a1[1]);//true

            using(var stream=new MemoryStream()){
                BinaryFormatter formatter = new BinaryFormatter();

                //先序列化再反序列化
                formatter.Serialize(stream,a1);
                stream.Position = 0;

                Singleton[] a2 = (Singleton[])formatter.Deserialize(stream);

                Console.WriteLine(a2[0]==a2[1]);//true
                Console.WriteLine(a1[0] == a1[1]);//true;
            }
            Console.ReadLine();
        }

 

3.流上下文(StreamingContext)

一组序列化好的对象有许多用处;同一个进程,同一台机器上的不同进程、不同机器上的不同进程。在一些比较少见的情况下,一个对象可能想知道它要在什么地方被反序列化,从而以不同的方式生成其形态。例如,如果对象中包装了Windows信号量(semaphone)对象,如果它知道要反序列化到同一个进程中,就可决定对它的内核句柄(kernal handle)进行序列化,这是因为内核句柄在同一个进程中有限。但如果要反序列化到同一台机器的不同进程中,那么可以决定对信号量的字符串名称序列化。最后,如果要反序列化到不同机器上,那么就可决定抛出异常,因为信号量只在同一台机器上有效。

SteamingContext有两个公共只读属性,如下所示:

Sate StreamingContextStates 一组位标志,指定要序列化/反序列化的对象的来源或目的地
Context Object 一个对象引用,对象中包含用户希望的任何上下文信息


通过State属性,就可判断序列化/反序列化的来源或目的地。

StreamingContextStates的标志:

标志说明 标志值 说明
CrossProcess 0x0001 来源或目的地是同一台机器的不同进程
CrossMachines 0x0002 来源或目的地在不同机器上
File 0x0004 来源或目的地是文件。不保证反序列化数据是同一个进程
Persistence 0x0008 来源或目的地是存储(store),比如数据库或文件。不保证反序列化数据的是同一个进程
Remoting 0x0010 来源或目的地是远程的未知未知。这个位置可能在(也可能不在)同一台机器上。
Other 0x0020 来源或目的地未知
Clone 0x0040 对象图被克隆。序列化代码可认为是由同一进程对数据进行反序列化,所以可安全地访问句柄或其他非托管设备。
CrossAppDomain 0x0080 来源或目的地是不通过的AppDomain
All 0x00FF 来源或目的地可能是上述任何一个上下文。这是默认设定

 


知道了如何获取这些信息后,接下来进行设置这些信息。在IFormatter接口(BinaryFormatter和SoapFormtter类型均实现该接口)定义了StreamingContext的可读/可写属性Context,构造格式化器时候,格式化器会初始化它的Context属性,将StreamingContextStates状态设置为All,将其对额外状态对象的引用设置为null。
接下来举如下栗子:

        private static Object DeepClone(Object original) {
            using(MemoryStream stream=new MemoryStream()){
                BinaryFormatter formatter = new BinaryFormatter();

                formatter.Context = new StreamingContext(StreamingContextStates.Clone);

                formatter.Serialize(stream,original);

                //定义到内存流的起始位置
                stream.Position = 0;

                return formatter.Deserialize(stream);
            }
        }

 

4.序列化代理

前面介绍了如何修改一个类型的实现,控制该类型如何对它本身的实例进行序列化和反序列化。然而,格式化器还允许不是“类型实现的一部分”的代码重写该类型“序列化和反序列化其对象”。这就是序列化代理。
要使这个机子工作起来,需要按照如下步骤:
a.首先要定义一个“代理类型”,它接管对现有类型的序列化和反序列化活动
b.向格式化器登记注册这个代理类型的实例,并告诉格式化器代理要作为的类型是什么。
c.一旦格式化器序列化/反序列化这个类型,那么将会调用由关联的代理类型关联的方法。

代理类型必须实现System.Runtime.Serialization.ISerializationSurrogate接口,该接口定义如下:

public interface ISerializationSurrogate{
    void GetObjectDate(Object obj,SerializationInfo info,StreamingContext context);
    Object SetObjectDate(Object obj,SerializationInfo info,StreamingContext context,ISurrogateSelector selector);
}

其中GetObjectDate在序列化时调用,SetObjectDate在反序列化时调用。

下面的栗子展示了如何使用代理类:

    internal sealed class UniversalToLocalTimeSerializationSurrogate : ISerializationSurrogate {
        public void GetObjectData(object obj,SerializationInfo info,StreamingContext context) {
            //将DateTime从本地时间转化为UTC
            info.AddValue("date", ((DateTime)obj).ToUniversalTime().ToString("u"));
        }

        public object SetObjectData(object obj,SerializationInfo info,StreamingContext context,ISurrogateSelector selector) {
            //将Datetime从UTC转化为本地时间
            return DateTime.ParseExact(info.GetString("date"),"u",null).ToLocalTime();
        }
    }

测试代码如下:
 

       static void Main(string[] args)
        {
            using(var stream=new MemoryStream()){
                //构造格式化器
                IFormatter formatter = new BinaryFormatter();

                //构造SurrogateSelector(代理选择器)对象
                SurrogateSelector ss = new SurrogateSelector();
                SurrogateSelector ss2 = new SurrogateSelector();
                ss.ChainSelector(ss2);

                //告诉代理选择器为DateTime对象使用指定的代理对象
                ss.AddSurrogate(typeof(DateTime),formatter.Context,new UniversalToLocalTimeSerializationSurrogate());
                //注意:addSurrogate可多次调用来登记代理

                //告诉格式化器使用代理选择器
                formatter.SurrogateSelector = ss;

                //创建一个DateTime对象代表本地时间,并序列化它
                DateTime localTimeBeforeSerialize = DateTime.Now;
                formatter.Serialize(stream,localTimeBeforeSerialize);

                //stream将Universal时间作为一个字符串显示,证明序列化成功
                stream.Position = 0;
                Console.WriteLine(new StreamReader(stream).ReadToEnd());

                //反序列化Universal字符串
                stream.Position = 0;
                DateTime localTimeAfterDeserialize = (DateTime)formatter.Deserialize(stream);

                Console.WriteLine("DateTimeBeforSerialize={0}", localTimeBeforeSerialize);//DateTimeAfterSerialize=2018/8/26 16:42:14
                Console.WriteLine("DateTimeAfterSerialize={0}", localTimeAfterDeserialize);//DateTimeAfterSerialize=2018/8/26 16:42:14

                Console.ReadLine();
            }
        }

 

posted @ 2018-08-30 19:58  HDWK  阅读(4763)  评论(0编辑  收藏  举报