Serializable 作用
一、序列化基础概念
序列化与 Serializable 标签
Serializable 是用于序列化的特性(attribute)。若要使一个对象能够被序列化,必须为其类添加 [System.Serializable] 标签,该标签表明这个类可以被序列化。例如:
[Serializable]
public class MyObject {
public int n1 = 0;
public int n2 = 0;
public String str = null;
}
序列化与反序列化的定义
对象通常暂时保存在内存中,无法直接通过 U 盘等介质转移。为了将对象的状态保存下来并进行转移,就需要进行序列化操作。简单来说,序列化就是把对象转换为可传输的介质的过程,就像把人的“魂”(对象)收伏成一个“石子”(可传输的介质)。
而反序列化则是相反的过程,即把介质中的内容还原成对象,如同把“石子”还原成人。
在进行序列化和反序列化操作时,对象所属的类必须能够被序列化,这就需要为类添加 [Serializable] 特性。
二、序列化的作用
便于网络传输和持久存储
在网络程序中,为了保证传输安全和高效,常常需要对对象进行序列化。同时,我们也经常需要将对象的字段值保存到磁盘中,并在以后检索此数据。如果不使用序列化,手动完成这项工作会非常繁琐且容易出错,尤其是在处理包含大量对象的大型业务应用程序时,程序员需要为每个对象编写代码来保存和还原字段及属性。而序列化提供了一种便捷的实现方式。
按值封送
在 .NET 中,对象仅在创建它的应用程序域中有效。除非对象是从 MarshalByRefObject 派生得到或标记为 Serializable,否则将对象作为参数传递或将其作为结果返回的尝试都会失败。
如果对象标记为 Serializable,它将被自动序列化,并从一个应用程序域传输到另一个应用程序域,然后进行反序列化,从而在第二个应用程序域中生成该对象的精确副本,这个过程称为按值封送。
如果对象是从 MarshalByRefObject 派生得到的,从一个应用程序域传递到另一个应用程序域的是对象引用,而不是对象本身。也可以将从 MarshalByRefObject 派生得到的对象标记为 Serializable,此时负责进行序列化并已预先配置为 SurrogateSelector 的格式化程序将控制序列化过程,并用一个代理替换所有从 MarshalByRefObject 派生得到的对象。如果没有预先配置为 SurrogateSelector,序列化体系结构将遵循标准序列化规则。
三、序列化的实现方式
基本序列化
要使一个类可序列化,最简单的方法是使用 Serializable 属性对其进行标记。以下是一个示例:
[Serializable]
public class MyObject {
public int n1 = 0;
public int n2 = 0;
public String str = null;
}
以下代码展示了如何将此类的一个实例序列化为一个文件:
MyObject obj = new MyObject();
obj.n1 = 1;
obj.n2 = 24;
obj.str = "一些字符串";
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Create, FileAccess.Write, FileShare.None);
formatter.Serialize(stream, obj);
stream.Close();
将对象还原到它以前的状态也很简单:
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Open, FileAccess.Read, FileShare.Read);
MyObject obj = (MyObject) formatter.Deserialize(stream);
stream.Close();
// 验证反序列化结果
Console.WriteLine("n1: {0}", obj.n1);
Console.WriteLine("n2: {0}", obj.n2);
Console.WriteLine("str: {0}", obj.str);
BinaryFormatter 效率很高,能生成非常紧凑的字节流,适用于在 .NET 平台上进行反序列化的对象。需要注意的是,对对象进行反序列化时并不调用构造函数,这是出于性能方面的考虑,但开发人员在将对象标记为可序列化时,应考虑这一特殊约定。
如果要求具有可移植性,可以使用 SoapFormatter,只需将格式化程序替换为 SoapFormatter,而 Serialize 和 Deserialize 调用不变。
另外,Serializable 属性无法继承。如果从一个已标记为可序列化的类派生出一个新的类,新类也必须使用该属性进行标记,否则将无法序列化。
选择性序列化
类通常包含不应被序列化的字段。例如,某个类用一个成员变量来存储线程 ID,当此类被反序列化时,序列化时存储的 ID 对应的线程可能不再运行,对这个值进行序列化没有意义。可以通过使用 NonSerialized 属性标记成员变量来防止它们被序列化:
[Serializable]
public class MyObject {
public int n1;
[NonSerialized] public int n2;
public String str;
}
自定义序列化
可以通过在对象上实现 ISerializable 接口来自定义序列化过程。这在反序列化后成员变量的值失效,但需要为变量提供值以重建对象的完整状态时非常有用。要实现 ISerializable,需要实现 GetObjectData 方法以及一个特殊的构造函数,在反序列化对象时会用到此构造函数。以下是一个示例:
[Serializable]
public class MyObject : ISerializable {
public int n1;
public int n2;
public String str;
public MyObject() {}
protected MyObject(SerializationInfo info, StreamingContext context) {
n1 = info.GetInt32("i");
n2 = info.GetInt32("j");
str = info.GetString("k");
}
public virtual void GetObjectData(SerializationInfo info, StreamingContext context) {
info.AddValue("i", n1);
info.AddValue("j", n2);
info.AddValue("k", str);
}
}
在序列化过程中调用 GetObjectData 时,需要填充 SerializationInfo 对象。在反序列化过程中,使用专门的构造函数将 SerializationInfo 传递给类。
如果从实现了 ISerializable 的类派生出一个新的类,只要新类中含有需要序列化的变量,就必须同时实现构造函数以及 GetObjectData 方法。
四、序列化过程的步骤
在格式化程序上调用 Serialize 方法时,对象序列化按照以下规则进行:
- 检查格式化程序是否有代理选取器。如果有,检查代理选取器是否处理指定类型的对象。如果选取器处理此对象类型,将在代理选取器上调用
ISerializable.GetObjectData。 - 如果没有代理选取器或有却不处理此类型,将检查是否使用
Serializable属性对对象进行标记。如果未标记,将会引发SerializationException。 - 如果对象已被正确标记,将检查对象是否实现了
ISerializable。如果已实现,将在对象上调用GetObjectData。 - 如果对象未实现
ISerializable,将使用默认的序列化策略,对所有未标记为NonSerialized的字段都进行序列化。
五、版本控制
.NET 框架支持版本控制和并排执行,但在向要跨版本序列化的类中添加或删除成员变量时,需要谨慎处理。特别是对于未实现 ISerializable 的类,若当前版本的状态发生了任何变化(如添加成员变量、更改变量类型或更改变量名称),可能会导致使用早期版本序列化的对象无法成功反序列化。
如果对象的状态需要在不同版本间发生改变,类的作者可以有两种选择:
- 实现
ISerializable,精确地控制序列化和反序列化过程,在反序列化过程中正确地添加和解释未来状态。 - 使用
NonSerialized属性标记不重要的成员变量。仅当预计类在不同版本间的变化较小时,才可使用这个选项。
六、序列化规则
由于类编译后便无法序列化,所以在设计新类时应考虑序列化。除以下情况外,最好将所有类都标记为可序列化:
- 所有的类都永远不会跨越应用程序域。如果某个类不要求序列化但需要跨越应用程序域,请从
MarshalByRefObject派生此类。 - 类存储仅适用于其当前实例的特殊指针。例如,如果某个类包含非受控的内存或文件句柄,请确保将这些字段标记为
NonSerialized或根本不序列化此类。 - 某些数据成员包含敏感信息。在这种情况下,建议实现
ISerializable并仅序列化所要求的字段。