• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

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

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

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

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

举点例子:

  • 应用程序的状态可以保存到一个磁盘文件或数据库中,并在应用程序下次运行时恢复。比如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:指示当前接口实施者是对另一个对象的引用。

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


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
用C#实现DES加密解密封装发布时间:2022-07-13
下一篇:
RGB,CMYK,HSB各种颜色表示的转换C#语言发布时间:2022-07-13
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap