序列化的概念
序列化 是将对象或对象图转化成字节流的过程。
反序列化 是将字节流转换回对象图的过程。
序列化的好处是,一旦将对象序列化成内存中的字节流,就可以方便的以一些更有用的方式处理数据,比如:
-
应用程序的状态可保存到磁盘文件或数据库中,并在应用程序下次允许时恢复。
-
一组对象可轻松通过网络发送给另一台机器上运行的程序。
.net 可以支持二进制序列化、XML和SOAP 序列化、JSON 序列化。其中二进制序列化功能最强大,类型保真,XML只序列化公共属性和字段,JSON 只序列化公共属性。JSON 广泛用于web 上共享数据,是一个开放式标准。
本篇只介绍.net 中二进制序列化。
如何使类型可序列化
看一个序列化应用的例子:
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
internal static class QuickStart {
public static void Main() {
// Create a graph of objects to serialize them to the stream
var objectGraph = new List<String> { "Jeff", "Kristin", "Aidan", "Grant" };
Stream stream = SerializeToMemory(objectGraph);
// Reset everything for this demo
stream.Position = 0;
objectGraph = null;
// Deserialize the objects and prove it worked
objectGraph = (List<String>) DeserializeFromMemory(stream);
foreach (var s in objectGraph) Console.WriteLine(s);
}
private static MemoryStream SerializeToMemory(Object objectGraph) {
// Construct a stream that is to hold the serialized objects
MemoryStream stream = new MemoryStream();
// Construct a serialization formatter that does all the hard work
BinaryFormatter formatter = new BinaryFormatter();
// Tell the formatter to serialize the objects into the stream
formatter.Serialize(stream, objectGraph);
// Return the stream of serialized objects back to the caller
return stream;
}
private static Object DeserializeFromMemory(Stream stream) {
// Construct a serialization formatter that does all the hard work
BinaryFormatter formatter = new BinaryFormatter();
// Tell the formatter to deserialize the objects from the stream
return formatter.Deserialize(stream);
}
}
格式化器是实现了 System.Runtime.Serialization.IFormatter 接口的类型,它知道如何序列化和反序列化对象图。BinaryFormatter 就是FCL 提供的格式化器。
BinaryFormatter 格式化器的Serialize 方法将对象转为流数据,它的Deserialize 方法将流数据转为对象。
格式化器的Deserialize 方法检查流的内容,构造流中所有对象的实例,并初始化所有这些对象中的字段。
反序列化的过程是,格式化器首先获取程序集标识信息,并通过调用System.Reflection.Assembly 的load 方法,确保程序集已加载到正在执行的AppDomain中。如果程序集中没有反序列化实例的类型就会报错。
下面是一个利用序列化实现对象的深拷贝:
private static Object DeepClone(Object original) {
// Construct a temporary memory stream
using (MemoryStream stream = new MemoryStream()) {
// Construct a serialization formatter that does all the hard work
BinaryFormatter formatter = new BinaryFormatter();
// This line is explained in this chapter's "Streaming Contexts" section
formatter.Context = new StreamingContext(StreamingContextStates.Clone);
// Serialize the object graph into the memory stream
formatter.Serialize(stream, original);
// Seek back to the start of the memory stream before deserializing
stream.Position = 0;
// Deserialize the graph into a new set of objects and
// return the root of the graph (deep copy) to the caller
return formatter.Deserialize(stream);
}
}
-
注意要保证代码为序列化和反序列化使用相同的格式化器。
-
可将多个对象图序列化到一个流中。
-
在反序列化时要注意当前AppDomain中有对应的类型。
下面说一说如何让类型可序列化
很简单,像下面这样向类型应用定制特性 System.SerializableAttribute 。
[Serializable]
internal struct Point {public Int32 x, y;}
-
SerializableAttribute 这个定制特性只能应用于引用类型 class、值类型 struct、枚举类型 enum、和委托 delegate。
-
枚举类型和 委托类型总是可序列化的,不必显示应用特性。
-
SerializableAttribute 不会被派生类型继承。
如何控制序列化和反序列化
将SerializableAttribute 定制特性应用于类型,所有实例字段都会被序列化。如果我不想让某一个字段序列化怎么办呢?。还有哪些情况不应该序列化呢?
-
windows 内核对象的句柄,它在反序列化到另一个进程或另一台机器上就会失去意义。
-
字段含有很容易计算的信息,去掉它可以减少传输的数据,增强性能。
使用 System.NonSerializedAttribute 定制特性指出类型中不应该序列化的字段。
[Serializable]
internal class Circle{
private Double m_radius; //半径
[NonSerialized]
private Double m_area; //面积
public Circle(Double radius){
m_radius = radius;
m_area = Math.PI * m_radius * m_radius;
}
...
}
除了NonSerializedAttribute 定制特性之外,FCL 还提供了其他控制序列化和反序列化的特性。这些特性应用于不同的场景,我这里只简单列举一些常用的。
System.Runtime.Serialization //这个命名空间有好多其他特性
OnDeserializedAttribute //根据字段值初始化瞬时状态
OnSerializingAttribute //序列化前,修改任何需要修改的状态
OnSerializedAttribute //序列化后,恢复任何需要恢复的状态
OnDeserializingAttribute //在类型新版本中为字段设置默认值
格式化器如何序列化类型实例
FCL在 System.Runtime.Serialization 命名空间提供了一个FormatterServices 类型。该类型只包含静态方法,而且该类型不能实例化。以下步骤描述了格式化器如何自动序列化类型,应用了 SerializableAttribute 特性的对象。
-
1 格式化器调用 FormatterServices 的 GetSerializableMembers方法:
通过反射获取public 和private实例字段,将它们放到MemberInfo 对象构成的数组中。
-
2 对象被序列化,System.Reflection.MemberInfo 对象数组传给 FormatterServices静态方法GetObjectData:
-
3 格式化器将程序 集标识和类型的完整名称写入流中。
-
4 格式化器遍历两个数组中的元素,将每个成员的名称和值写入流中。
以下步骤描述格式化器如何自动反序列化类型应用了SerializableAttribute 特性的对象。
-
格式化器从流中读取程序集标识和完整类型名称。将该程序集加载到AppDomain中。
-
格式化器调用 FormatterServices 的静态方法GetUninitializedObject:
这个方法为新对象分配内存,但不为对象调用构造器。
-
格式化器现在构造并初始化一个MemberInfo 数组,具体做法和前面一样,都是调用 FormatterServices的 GetSerializableMembers方法。
-
格式化器根据流中包含的数据创建并初始化一个Object 数组。
-
将新分配对象、MemberInfo数组以及并行Object数组的引用传给FormatterServices的静态方法 PopulateObjectMembers:
这个方法遍历数组,将每个字段初始化成对应的值,到此为止,对象就算是被彻底反序列化了。
控制序列化/反序列化的数据
如果前面提到的特性:OnSerialized、OnDeserializing、OnDeserialized等,它们不能满足你想要的功能。另外格式化器使用的是反射,反射会影响程序的性能。为了对序列化/和反序列化的数据进行完全的控制,避免使用反射,你的类型可以实现 System.Runtime.Serialization.ISerializable 接口。它的定义如下:
public interface ISerializable {
void GetObjectData(SerializationInfo info , StreamingContext context);
}
它只有一个方法GetObjectData,实现这个接口的大多数类型还实现了一个特殊的构造器。
类型一旦实现了这个接口,所有派生类型也要实现它,而且派生类型必须保证调用基类的GetObjectData方法和特殊构造器。
格式化器序列化对象图时会检查每个对象。如果发现一个对象的类型实现了 ISerializable接口,就会忽略所有定制特性,改为构造新的System.Runtime.Serialization.SerializationInfo 对象。该对象包含了要为对象序列化的值的集合。
要实现ISerializable 但基类没有实现怎么办?
这种情况下,派生类必须手动序列化基类的字段,具体的做法就是获取它们的值,并把者些值添加到SerializationInfo 集合中。然后,在你的特殊构造器中,还必须从集合中取出值,以某种方式设置基类的字段。如果基类的字段是public 或 protected 的可以实现,但是private 字段,几乎不能实现。
流上下文
一组序列化好的对象可以有许多目的地:同一个进程、同一台机器的不同进程、不同机器上的不同进程等。
某些情况下,一个对象想知道它要在什么地方反序列化,从而以不同的方式生成它的状态。
例如,如果对象中包装了Windows 信号量对象,如果它知道要反序列化到同一个进程中,就可决定对它的内核句柄进行序列化,这是因为内核句柄在一个进程中有效。 但如果要反序列化到同一台计算机的不同进程中,就可决定对信号量的字符串名称进行序列化。 最后,如果要反序列化到不同计算机的进程,就可决定抛出异常,因为信号量只在一台机器中有效。
格式化器中的大量方法都接受一个StreamingContext 类型,StreamingContextStates 结构是一个非常简单的值类型,它只提供两个公共只读属性。
接受一个StreamingContext 结构的方法能检查State 属性的位标志,判断要序列化/反序列化的对象的来源或目的地。
IFormatter 接口定义了StreamingContext类型的可读/可写性Context。构造格式化器时,格式化器会初始化它的 Context属性,将StreamingContextStates 设为All,对额外状态对象的引用设为null。
.net 由于元数据和IL 中间语言以及JIT 编译等特点使它在序列化方面有更多的灵活性。在.net 中类型还能序列化为不同类型,对象也能反序列化为不同的对象。.net 中的格式化器还允许不是 “类型实现的一部分” 的代码重写该类型 “序列化和反序列化其对象” 的方式。