在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言:写此文章一方面是为了巩固对序列化的认识,另一方面是因为本人最近在面试,面试中被问到“为什么要序列化”。虽然一直在使用,自己也反复的提到序列化,可至于说为什么要序列化,还真的没想过,所以本文就这样产生了。 序列化是将一个对象转换成一个字节流的过程。反序列化是将一个字节流转换回对象的过程。在对象和字节流之间转换是很有用的一个机制。(当然这个还不能回答它的实际用处) 举点例子:
除了上述的几个场景,我们可以将系列化得到的字节流进行任意的操作。 一、序列化、反序列化快速实践 [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等)都会被序列化。但是,有时候类型中定义了一些不应序列化的实例字段。 一般情况下,以下两种情况不希望序列化字段:
使用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步骤:
Deserialize步骤:
四、控制序列化/反序列化的数据 本文上述,有提到如何使用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的GetObjectData方法和特殊的构造器: [Serializable] class Base { protected string name = "Mario"; public Base() { } } [Serializable] class Derived : Base, ISerializable { private DateTime _date = DateTime.Now; public Derived() { } 在代码中,有一个名为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:指示当前接口实施者是对另一个对象的引用。 好了,序列化和反序列化的东西说的也差不多了,大家有什么更好的想法可以和我交流。 |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论