C#之你懂得的序列化/反序列化

前言:写此文章一方面是为了巩固对序列化的认识,另外一方面是由于本人最近在面试,面试中被问到“为何要序列化”。虽然一直在使用,本身也反复的提到序列化,可至于说为何要序列化,还真的没想过,因此本文就这样产生了。面试

序列化是将一个对象转换成一个字节流的过程。反序列化是将一个字节流转换回对象的过程。在对象和字节流之间转换是颇有用的一个机制。(固然这个还不能回答它的实际用处)数据库

举点例子:数组

  • 应用程序的状态能够保存到一个磁盘文件或数据库中,并在应用程序下次运行时恢复。好比ASP.NET就是利用系列化和反序列化保存和恢复回话状态。
  • 一组对象能够轻松复制到系统的剪切板,而后再粘贴到其余的地方(应用程序)。
  • 一组对象可克隆并放到其余地方做为备份。
  • 一组对象能够经过网络发送给另外一台机器上运行的进程(好比Remoting)。

 除了上述的几个场景,咱们能够将系列化获得的字节流进行任意的操做。网络

 1、序列化、反序列化快速实践ide

    [Serializable]
    class MyClass
    {
        public string Name { get; set; }
    }

一个自定义类,切记须要加上[Serializable]特性(可应用于class、struct、enum、delegate)。函数

        private static MemoryStream SerializeToMemoryStream(object objectGraph)
        {
            //一个流用来存放序列化对象
            var stream = new MemoryStream();
            //一个序列化格式化器
            var formater = new BinaryFormatter();
            //将对象序列化到Stream中
            formater.Serialize(stream, objectGraph);
            return stream;
        }

        private static object DeserializeFromMemory(Stream stream)
        {
            var formater = new BinaryFormatter();
            return formater.Deserialize(stream);
        }

SerializeToMemoryStream为序列化方法,此处经过BinaryFormatter类将对象序列化到MemoryStream中,而后返回Stream对象。性能

DeserizlizeFromMemory为反序列化方法,经过传入的Stream,而后使用BinaryFormatter的Deserialize方法反序列化对象。测试

除了可使用BinaryFormatter进行字节流的序列化,还可使用XmlSerializer(将对象序列为XML)和DataContratSerializer。字体

Serialize的第二个参数是一个对象的引用,理论上应该能够是任何类型,无论.net的基本类型仍是其余类型或者是咱们的自定义类型。若是是对象和对象的引用关系,Serizlize也是能够一直序列化的,并且Serialize会很智能的序列化每一个对象都只序列化一次,防止进入无限循环。this

P.S. 1.Serialze方法其实能够将对象序列化为Stream,也就意味着不只能够序列化为MemoryStream,还能够序列化为FIleStream或者是其余继承自Stream的类型。

      2.除了上述的将一个对象序列化到一个Stream,也能够将多个对象序列化中,仍是调用Serialize方法,第二个参数为不一样的对象便可;在反序列化的时候一样的方法,只不过      强转的类型指定为须要的便可。

 序列化多个对象到Stream:

            MyClass class1 = new MyClass();
            MyClass2 class2=new MyClass2();
            formater.Serialize(stream,class1);
            formater.Serialize(stream,class2);

从Stream中反序列化多个对象:

            MyClass class1 =(MyClass) formater.Deserialize(stream);
            MyClass1 class2 = (MyClass1)formater.Deserialize(stream);

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

若是给类添加了SerializeAttribute,那么类的全部实例字段(private、protected、public等)都会被序列化。可是,有时候类型中定义了一些不该序列化的实例字段。

通常状况下,如下两种状况不但愿序列化字段:

  • 字段含有反序列化后变得无效的信息。例如,假定一个对象包含到一个Windows内核对象(如文件、进程、线程、事件等),那么在反序列化到另外一个进程或另外一台机器以后,就会失去意义。
  • 字段含有很容易计算的信息。在这种状况下,要选出那些无需序列化的字段,减小须要传输的数据,从而加强应用程序的性能。

使用NonSerializedAttribute特性来指明哪些字段无需序列化。

     [NonSerialized]
        private string _name;

p.s.[NoSerialized] 仅仅能添加在字段,或者是没有get和set访问器属性上,对于有get和set这样的属性使用是不行的。不要紧使用[ScriptIgnore]特性标识属性则能够忽略JSON这样的序列化、使用[XmlIgnoreAttribute]特性标识属性则能够忽略XmlSerializer的序列化操做。

虽然使用NonSerizlized特性可使字段不被序列化,可是在序列化或者反序列化的时候每每都会把值清空,或者是没有一些但愿的默认值,还好咱们可使用其余的特性来辅助完成。

修改下上文中的MyClass:

[Serializable]
    class MyClass
    {
        [NonSerialized]
        public string _name;

        [OnDeserialized]
        private void OnDeserialized(StreamingContext context)
        {
            _name = "Mario";
        }

        [OnDeserializing]
        private void OnDeserializing(StreamingContext context)
        {
            _name = "super";
        }

        [OnSerializing]
        private void OnSerializing(StreamingContext context)
        {
            _name = "listen";
        }

        [OnSerialized]
        private void OnSerialized(StreamingContext context)
        {
            _name = "fly";
        }

        public void Print()
        {
            Console.WriteLine(_name);
        }
    }

 在类中一共使用了四个特性,OnDeserialized、OnDeserializing、OnSerializing、OnSerialized,分别是反序列化后、反序列化前、序列化前、序列化后。不过,若是同时指定了OnDeserialized和OnDeserializing,那么结果应该是OnDeserialized中的逻辑;同理,若是同时指定了OnSerializing和OnSerialized,那么结果应该是OnSerialized中的逻辑。另外,在一个类中,仅仅能指定一个方法为上述中的一个特性(即OnSerialized特性只能被一个方法使用、OnSerialized特性只能被一个方法使用,其他两个同理),不然序列化或者反序列化则会出现异常。

P.S. 这些方法一般为private的,而且参数为StreamingContext。

       MyClass class1 = new MyClass();
            var stream = SerializeToMemoryStream(class1);
            class1.Print();
            stream.Position = 0;
            class1 = (MyClass)DesrializeFromMemory(stream);
            class1.Print();
            Console.Read();

 运行上述调用能够发现,虽然咱们没有将name属性序列化,可是在序列化/反序列化以后仍是能够输出值的,若是你同时指定了OnDeserializing和OnDeserialized或者同时指定了OnSerializing和OnSerialized,那么你会发现使用的都是后者的值,这也验证了上述中的解释。

有时候咱们的类可能会增长字段,但是呢,咱们已经序列化好的数据是旧的版本,因此在反序列化的时候就会出现异常,还好咱们也有办法,给新加的字段都增长一个OptinalFieldAttribute特性,这样当格式化器看到该attribute应用于一个字段时,就不会由于流中的数据不包含这个字段而出现异常。

3、序列化和反序列化的原理

为了简化格式化器的操做,在System.Runteime.Serialization中有一个FormatterServices类型。该类型只包含静态方法,而且该类为静态类。

Serialize步骤:

  • 格式化器调用FormatterServices的GetSerializableMembers方法:
    public static MemberInfo[] GetSerializableMembers(Type type,StreamContext context);

    这个方法利用反射获取类型的public和private实例字段(除了标识为NonSerializedAttribute的字段除外)。方法返回由MemberInfo对象构成的一个数组,其中每一个元素都对应于一个可序列化的实例字段。

  • 对象被序列化,MemberInfo对象数组传给FormatterServices的静态方法GetObjectData:
    public static object[] GetObjectData(Object obj,MemberInfo[] members);

    这个方法返回一个Object数组,其中每一个元素都标识了被序列化的那个对象的一个字段的值。这个Object数组和MemberInfo数组是并行的;也就是说,Object数组中的元素0是MemberInfo数组中的元素0所标识的那个成员的值。

  • 格式化器将程序集标识和类型的完整名称写入流中。
  • 格式化器而后遍历两个数组中的元素,将每一个成员的名称和值写入流中。

Deserialize步骤:

  • 格式化器从流中读取程序集标识和完整类型名称。若是程序集当前没有加载到AppDomain中,就加载它。若是程序集不能加载,则出现异常。若是程序集已经加载,格式化器将程序集标识信息和类型全名传给FormatterServices的静态方法GetTypeFromAssembly:
    public static Type GetTypeFromAssembly(Assembly assembly, string name);

    这个方法返回一个Type对象,表明要反序列化的那个对象的类型。

  • 格式化器调用FormatterServices的静态方法GetUninitializedObject:
    public static Object GetUninitializedObject(Type type);

    这个方法为一个新对象分配内存,并不为对象调用构造函数。因此,对象的全部字段都被初始化为null或者0;

  • 格式化器如今构造并初始化一个MemberInfo数组,一样是调用FormatterServices的GetSerializableMembers方法。这个方法返回序列化好,须要反序列化的一组字段。
  • 格式化器根据流中包含的数据建立并初始化一个Object数组。
  • 将对新分配的对象、MemberInfo数组以及并行Object数组的传给FomatterServices的静态方法PopulateObjectMembers:
    public static Object PopulateObjectMembers(Object obj,MemberInfo[] members, Object [] data);

    这个方法遍历数组,将每一个字段初始化成对应的值。到这里,就算反序列化结束了。

4、控制序列化/反序列化的数据

本文上述,有提到如何使用OnSerializing、OnSerialized、OnDeserializing、OnDeserialized以及NonSerialized和OptionalField特性进行控制序列化和反序列化。可是,格式化器内部使用反射,而反射的速度是比较慢的,因此增长了序列化和反序列化对象所花的时间。为了对序列化和反序列化彻底的控制,而且不使用反射,那么咱们的类型能够实现ISerializable接口,此接口仅仅有一个方法:

public Interface ISerializable
{
  void GetObjectData(SerializationInfo info, StreamContext context);
}

一旦类型实现了此接口,全部派生类型也必须实现它,并且派生类型必须保证调用基类的GetOBjectData方法和特殊的构造器。除此以外,一旦类型实现了该接口,则永远不能删除它,不然会失去与派生类的兼容性。

ISerializable接口和特殊构造器旨在由格式化器使用。可是,任何代码均可能调用GetObjectData,则可能返回敏感数据。另外,其余代码可能构造一个对象,并传入损坏的数据。所以,建议将以下的attribute应用于GetObjectData方法和特殊构造器:

[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]

 格式化器序列化一个对象时,会检查每一个对象。若是发现一个对象的类型实现了ISerializable接口,格式化器就会忽略全部定制attribute,改成构造一个新的SerializationInfo对象,这个对象包含了要实际为对象序列化的值的集合。

构造一个SerializationInfo时,格式化器要两个参数:Type和IFormatterConverter。Type参数标识要序列化的对象。为了惟一性地标识一个类型,须要两个部分的信息:类型的字符串名称及其程序集的标识。一个SerializationInfo对象构造好以后,会包含类型的全名(即Type的FullName),并将这个字符串存储到一个私有字段中。为了获取类型的全名,可以使用SerializationInfo的FullTypeName属性。经过调用SerializationInfo的SetType方法,传递目标Type对象的引用,用于设置FullTypeName和AssemblyName属性。

构造好并初始化SerializationInfo对象后,格式化器调用类型的GetObjectData方法,传递SeriializationInfo对象。GetObjectData方法负责决定须要序列化的信息,而后将这些信息添加到SerializationInfo中。GetObjectData调用SerializationInfo类型的AddValue方法来指定要序列化的信息。须要对每一个要添加的数据,都进行AddValue方法的调用。 

下面代码展现了Dictionary<TKey,TValue>类型如何实现ISerializable和IDeserializationCallback接口来控制其对象的序列化和反序列化工做。

4、在基类没有实现ISerializable的状况下定义一个实现它的类型

以前提到,若是基类实现了ISerializable接口,那么它的派生类也必须实现ISerializable接口,同时还要调用基类的GetObjectData方法和特殊构造器。(见上文红色字体)
可是,你可能要定义一个类型来控制它的序列化,但它的基类没有实现ISerializable接口。在这种状况下,派生类必须手动序列化基类的字段,具体的作法是获取它们的值,并把这些值添加到SerializationInfo集合中。而后,在特殊构造器中,还必须从集合中取出值,并以某种方式设置基类的字段。若是基类的字段是public或者protected字段,还容易实现。但,若是基类的private字段,那么则很难实现。

如下代码实现如何正确实现ISerializable的GetObjectData方法和特殊的构造器:

    [Serializable]
        class Base
        {
            protected string name = "Mario";
            public Base()
            {
            }
        }

        [Serializable]
        class Derived : Base, ISerializable
        {
            private DateTime _date = DateTime.Now;
            public Derived() { }

      //若是这个构造器不存在,则会引起一个SerializationException异常
      //若是此类不是密封类,这个构造器就应该是protected的 [SecurityPermission(SecurityAction.Demand, SerializationFormatter
= true)] private Derived(SerializationInfo info, StreamingContext context) { Type baseType = this.GetType().BaseType; MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType, context); for (int i = 0; i < memberInfos.Length; i++) { FieldInfo fieldInfo = (FieldInfo)memberInfos[i]; fieldInfo.SetValue(this, info.GetValue(baseType.FullName + "+" + fieldInfo.Name, fieldInfo.FieldType)); } _date = info.GetDateTime("Date"); } [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] public virtual void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Data", _date); Type baseType = this.GetType().BaseType; MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType,context); for (int i = 0; i < memberInfos.Length; i++) { info.AddValue(baseType.FullName + "+" + memberInfos[i].Name, ((FieldInfo)memberInfos[i]).GetValue(this)); } } public override string ToString() { return string.Format("Name={0},Date={}", name, _date); } }

在代码中,有一个名为Base的基类,它只用Serializable特性标识。其派生类Derived类,也使用了Serializable特性,同时还实现了ISerializable接口。同时两个类还定义了本身的字段,调用SerializationInfo的AddValue方法进行序列化和反序列化。

 

解释:

序列化: 每一个AddValue方法都获取一个String名称和一些数据。数据通常是简单的类型,固然咱们也能够传递object引用。GetObjectData添加好全部必要的序列化信息以后,会返回至格式化器。如今,格式化器获取已经添加到SerializationInfo对象的全部值,并把它们都序列化到流中。同时,咱们还向GetObjectData方法中传递了另一个参数StreamingContext对象的实例。固然,大多数类型的GetObjectData方法都忽略了此参数,下文详细说明。

反序列化:格式化器从流中提取一个对象时,会为新对象分配内存(经过FormatterService.GetUninitializedObject方法)。最初,此对象的全部字段都为0或者是null。而后,格式化器检查类型是否实现了ISerializable接口。若是存在此接口,格式化器则会尝试调用咱们定义的特殊构造函数,它的参数和GetObjectData是一致的。

若是类是密封类,则建议将此特殊构造声明为private,这样就能够防止其余代码调用它。若是不是密封类,则应该将这个特殊构造器声明为protected,保证派生类能够调用它。切记,不管这个特殊构造器是如何声明的,格式化器均可以调用它的。

构造器获取对一个SerializationInfo对象的引用,在这个SerializationInfo对象中,包含了对象(要序列化的对象)序列化时添加的全部值。特殊构造器可调用GetBoolean,GetChar,GetByte,GetInt32和GetValue等任何一个方法,向他传递与序列化一个值所用的名称对应的一个字符串。以上的每一个方法返回的值再用于初始化新对象的各个字段。

反序列化一个对象的字段时,应调用和对象序列化时传给AddValue方法的值得类型匹配的一个Get方法。也就是说,若是GetObjectData方法调用AddValue时传递的是一个Int32值,那么在反序列化对象的时候,也应该为同一个值调用GetInt32方法。若是值在流中的类型和你要获取的类型不匹配,格式化器则会尝试用IFormatterConverter对象将流中的值转换为你指定的类型。

上文中提到,构造SerializationInfo对象时,须要传递Type和IFormatterConverter接口的对象(此时,它是重点,不要被Type勾引走)。因为格式化器负责构造SerializationInfo对象,因此要由它选择它须要的IFormatterConverter。.Net的BinaryFormatter和SoapFormatter构造的就是一个FormatterConverter类型,.Net的格式化器没有提供一个让你能够选择的IFormatterConverter的实现。

FormatterConverter类型调用System.Convert类的各类静态方法在不一样的类型之间进行转换,好比讲一个Int16转换为Int32。然而,为了在其余任意类型之间转换一个值,FormatterConverter须要调用Convert的ChangeType方法将序列化好的类型转换为一个IConvertible接口,而后再调用恰当的接口的方法。因此,要容许一个可序列化类型的对象反序列化成一个不一样的类型,能够考虑让本身的类型实现IConvertible接口。切记,只有在反序列化对象时调用Get方法,而且发现了类型和流中的值得类型不匹配时候,才会使用FormatterConverter对象。

特殊构造器也能够不调用上面的各类Get方法,而是调用GetEnumerator。此方法会返回一个SerializationInfoEnumerator对象,可以使用该对象遍历SerializationInfo对象中包含的全部的值。枚举的每一个值都是一个SerializationEntry对象。

固然,咱们彻底能够自定义一个类型,让它实现ISerializable的GetObjectData方法和特殊构造器一个类型派生。若是咱们的类型实现了ISerializable,那么能够在咱们实现的GetObjectData方法和特殊构造器中,必须调用基类中的同名方法,以确保对象正确序列化和反序列化。这一点是必须的哦,不然对象时不能正确序列化和反序列化。

若是咱们的派生类型中没有其余的额外字段,固然也没有特殊的序列化和反序列化需求,就不用事先ISerializable接口。和其余接口成员类似,GetObjectData是virtual的,调用它能够正确的序列化对象。格式化器将特殊构造器视为“已虚拟化”,也就是说,反序列化过程当中,格式化器会检查要实例的类型,若是那个类型没有提供特殊的特殊构造器,则会看其基类是否存在,知道找到一个实现了特殊构造器的一个类。

 

注意:特殊构造器中的代码通常会从传给 它的SerializationInfo对象中提取字段。提取了字段后,不能保证对象已彻底反序列化,因此特殊构造器中的代码不该尝试操纵它提取的对象。若是咱们的类型必须访问提取的一个对象中的成员,最好咱们的类型提供一个应用了OnDeserialized特性的方法,或者让咱们的类型实现IDeserializationCallback接口的OnDeserialization方法。调用该方法时,全部对象的字段都已经设置好。然而,对于多个对象来讲,它们的OnDeserialized或OnDeserialization方法的调用顺序是没有保障的。因此,虽然字段可能已经初始化,但咱们仍然不知道被引用的对象是否已彻底反序列化好(若是那个被引用的对象也提供了一个OnDeserialized方法或者实现了IDeserializationCallback)。

P.S. 必须调用AddValue方法的某个重载版本为本身的类型添加序列化信息。若是一个字段的类型实现了ISerializable接口,就不要在字段上调用GetObjectData,而应该调用AddValue来添加字段。格式化器会发现字段的类型实现了ISerializable,会自动调用GetObjectData。若是本身在字段上调用了GetObjectData,格式化器则不会知道在对流进行反序列化时建立一个新对象。

5、将类型序列化为不一样的类型以及将对象反序列化为不一样的对象

      [Serializable]
        public class Student : ISerializable
        {
            private string _name;

            public string Name
            {
                get { return _name; }
                set { _name = value; }
            }
            [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
            public void GetObjectData(SerializationInfo info, StreamingContext context)
            {
                info.SetType(typeof(SerializationHelper));
            }
        }

        [Serializable]
        public class SerializationHelper : IObjectReference
        {
            public object GetRealObject(StreamingContext context)
            {
                return "新的类型哦";
            }
        }

上述代码中一个咱们的数据类Student,还有一个序列化帮助类,其中Student类就是咱们要序列化的类,帮助类就是为了告诉代码咱们要把Student类序列化为它,而且再反序列化的时候也应该是它。
测试下:

   static void Main(string[] args)
        {
            Student student = new Student { Name = "马里奥" };
            using (var stream = new MemoryStream())
            {
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(stream, student);
                stream.Position = 0;

                var deserializeValue = formatter.Deserialize(stream);
                Console.Write(deserializeValue.ToString());
                Console.Read();
            }
        }

能够看到结果:

P.S. ISerializable:容许对象控制其本身的序列化和反序列化过程。

   IObjectReference:指示当前接口实施者是对另外一个对象的引用。

好了,序列化和反序列化的东西说的也差很少了,你们有什么更好的想法能够和我交流。

相关文章
相关标签/搜索