zoukankan      html  css  js  c++  java
  • C#之你懂得的序列化/反序列化

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

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

    举点例子:

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

     除了上述的几个场景,我们可以将系列化得到的字节流进行任意的操作。

     一、序列化、反序列化快速实践

        [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会很智能的序列化每个对象都只序列化一次,防止进入无限循环。

    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);

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

    如果给类添加了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应用于一个字段时,就不会因为流中的数据不包含这个字段而出现异常。

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

    为了简化格式化器的操作,在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);

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

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

    本文上述,有提到如何使用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接口来控制其对象的序列化和反序列化工作。

    四、在基类没有实现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,格式化器则不会知道在对流进行反序列化时创建一个新对象。

    五、将类型序列化为不同的类型以及将对象反序列化为不同的对象

          [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:指示当前接口实施者是对另一个对象的引用。

    好了,序列化和反序列化的东西说的也差不多了,大家有什么更好的想法可以和我交流。

  • 相关阅读:
    《Machine Learning in Action》—— 白话贝叶斯,“恰瓜群众”应该恰好瓜还是恰坏瓜
    《Machine Learning in Action》—— 女同学问Taoye,KNN应该怎么玩才能通关
    《Machine Learning in Action》—— Taoye给你讲讲决策树到底是支什么“鬼”
    深度学习炼丹术 —— Taoye不讲码德,又水文了,居然写感知器这么简单的内容
    《Machine Learning in Action》—— 浅谈线性回归的那些事
    《Machine Learning in Action》—— 懂的都懂,不懂的也能懂。非线性支持向量机
    《Machine Learning in Action》—— hao朋友,快来玩啊,决策树呦
    《Machine Learning in Action》—— 剖析支持向量机,优化SMO
    《Machine Learning in Action》—— 剖析支持向量机,单手狂撕线性SVM
    JVM 字节码指令
  • 原文地址:https://www.cnblogs.com/ListenFly/p/3512629.html
Copyright © 2011-2022 走看看