第 24 章 运行时序列化

第 24 章 运行时序列化

本章内容

序列化是将对象或对象图转换成字节流的过程。反序列化是将字节流转换回对象图的过程。在对象和字节流之间转换是很有用的机制。下面是一些例子。

24.1 序列化/反序列化快速入门

格式化器参考对每个对象的类型进行描述的元数据,从而了解如何序列化完整的对象图。序列化时,Serialize 方法利用反射来查看每个对象的类型中都有哪些实例字段。在这些字段中,任何一个引用了其他对象,格式化器的 Serialize 方法就知道那些对象也要进行序列化。如果对象图中的两个对象相互引用,格式化器会检测到这一点,每个对象都只序列化一次,避免发生死循环。

面是一个有趣而实用的方法,它利用序列化创建对象的深拷贝(或者说克隆体):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static Object DeepClone(Object original) {
// 构造临时内存流
using (MemoryStream stream = new MemoryStream()) {

// 构造序列化格式化器来执行所有实际工作
BinaryFormatter formatter = new BinaryFormatter();

// 值一行在本章 24.6 节“流上下文” 解释
formatter.Context = new StreamingContext(StreamingContextStates.Clone);

// 将对象图序列化到内存流中
formatter.Serialize(stream, original);

// 反序列化前,定位到内存流的起始位置
stream.Position = 0;

// 将对象图反序列化成一组新对象,
// 向调用者返回对象图(深拷贝)的根
return formatter.Deserialize(stream);
}
}

序列化对象时,类型的全名和类型定义程序集的全名会被写入流。反序列化对象时,格式化器首先获取程序集标识信息。并通过调用 System.Refleciton.AssemblyLoad方法,确保程序集已加载到正在执行的 AppDomain 中。

24.2 使类型可序列化

开发者必须向类型应用定制特性 System.SerializableAttribute

1
2
[Serializable]
internal struct Point { public Int32 x, y; }

SerializableAttribute 这个定制特性只能应用于引用类型(class)、值类型(struct)、枚举类型(enum)和委托类型(delegate)。注意,枚举和委托类型总是可序列化的,所以不必显式应用 SerializableAttribute 特性。除此之外,SerializableAttribute 特性不会被派生类型继承。

24.3 控制序列化和反序列化

将 SerializableAttribute 定制特性应用于类型,所有实例字段(public,private 和 protected等)都会被序列化①。但类型可能定义了一些不应序列化的实例字段。一般有两个原因造成我们不想序列化部分实例字段。

① 在标记了 [Serializable] 特性的类型中,不要用 C#的“自动实现的属性”功能来定义属性。这是由于字段名是由编译器自动生成的,而生成的名称每次重新编译代码时都不同。这会阻止类型被反序列化。

  • 字段含有反序列化后变得无效的信息。例如,假定对象包含 Windows 内核对象(
  • 字段含有很容易计算的信息。

以下代码使用 System.NonSerializedAttribute 定制特性指出类型中不应序列化的字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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;
}

...
}

注意,该特性只能应用于类型中的字段,而且会被派生类型继承。

当流反序列化成 Circle 对象的 m_radius 字段会被设为 10,但它的 m_area 字段会被初始化成 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[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;
}

[OnDeserialized]
private void OnDeserialized(StreamingContext context) {
m_area = Math.PI * m_radius * m_radius;
}
}

修改过的 Circle 类包含一个标记了 System.Runtime.Serialization.OnDeserializedAttribute 定制特性的方法。每次反序列化类型的实例,格式化器都会检查类型中是否定义了应用了该特性的方法。如果是,就调用该方法。调用这个方法时,所有可序列化的字段都会被正确设置。在该方法中,可能需要访问这些字段来执行一些额外的工作,从而确保对象的完全反序列化。

除了 OnDeserializedAttribute 这个定制特性,System.Runtime.Serialization 命名空间还定义了包括 OnSerializingAttributeOnSerializedAttributeOnDeserializingAttribute 在内的其他定制特性。可将它们应用于类型中定义的方法,对序列化和反序列化过程进行更多的控制。在下面这个类中,这些特性被应用于不同的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[Serializable]
public class MyType {
Int32 x, y; [NonSerialized] Int32 sum;

public MyType(Int32 x, Int32 y) {
this.x = x; this.y = y; sum = x + y;
}

[OnDeserializing]
private void OnDeserializing(StreamingContext context) {
// 举例:在这个类型的新版本中,为字段设置默认值
}

[OnDeserialized]
private void OnDeserialized(StreamingContext context) {
// 举例:根据字段值初始化瞬时状态(比如 sum 的值)
sum = x + y;
}

[OnSerializing]
private void OnSerializing(StreamingContext context) {
// 举例:在序列化前,修改任何需要修改的状态
}

[OnSerialized]
private void OnSerialized(StreamingContext context) {
// 举例:在序列化后,恢复任何需要恢复的状态
}
}

类型中新增的每个字段都要应用 OptionalFieldAttribute 特性。然后,当格式化器看到该特性应用于一个字段时,就不会因为流中的数据不包含这个字段而抛出 SerializationException.

24.4 格式化器如何序列化类型实例

以下步骤描述了格式化器如何自动序列化类型应用了 SerializableAttribute特性的对象。

  1. 格式化器调用 FormatterServicesGetSerializableMembers 方法:
    public static MemberInfo[] GetSerializableMembers(Type type, StreamingContext context);
    这个方法利用反射获取类型的 publicprivate 实例字段(标记了 NonSerializedAttribute 特性的字段除外)。方法返回由 MemberInfo 对象构成的数组,其中每个元素都对应一个可序列化的实例字段。

  2. 对象被序列化,System.Reflection.MemberInfo 对象数组传给 FormatterServices 的静态方法 GetObjectData:
    public static Object[] GetObjectData(Object obj, MemberInfo[] members);
    这个方法返回一个 Object 数组,其中每个元素都标识了被序列化的那个对象中的一个字段的值。这个 Object 数组和 MemberInfo 数组是并行(parallel)的;换言之,Object 数组中元素 0 是 MemberInfo 数组中的元素 0 所标识的那个成员的值。

  3. 格式化器将程序集标识和类型的完整名称写入流中。

  4. 格式化器然后遍历两个数组中的元素,将每个成员的名称和值写入流中。

以下步骤描述了格式化器如何自动反序列化类型应用了 SerializableAttribute 特性的对象。

  1. 格式化器从流中读取程序集标识和完整类型名称。如果程序集当前没有加载到 AppDomain 中,就加载它。如果程序集已加载,格式化器将程序集标识信息和类型全名传给 FormatterServices 的静态方法 GetTypeFromAssembly:
    public static Type GetTypeFromAssembly(Assembly assem, String name);
    这个方法返回一个 System.Type 对象,它代表要反序列化的那个对象的类型。

  2. 格式化器调用 FormmatterServices 的静态方法 GetUninitializedObject:
    public static Object GetUninitializedObject(Type type);
    这个方法为一个新对象分配内存,但不为对象调用构造器。然而,对象的所有字节都被初始为 null0

  3. 格式化器现在构造并初始化一个 MemberInfo 数组,具体做法和前面一样,都是调用 FormatterServicesGetSerializableMembers 方法。这个方法返回序列化好、现在需要反序列化的一组字段。

  4. 格式化器根据流中包含的数据创建并初始化一个 Object 数组。

  5. 将新分配对象、MemberInfo 数组以及并行 Object 数组(其中包含字段值)的引用传给 FormatterServices 的静态方法 PopulateObjectMembers
    public static Object PopulateObjectMembers(Object obj, MemberInfo[] members, Object[] data);
    这个方法遍历数组,将每个字段初始化成对应的值。到此为止,对象就算是被彻底反序列化了。

24.5 控制序列化/反序列化的数据