序列化的attribute,是为了利用序列化的技术
准备用于序列化的对象必须设置 [System.Serializable] 标签,该标签指示一个类能够序列化。
便于在网络中传输和保存
这个标签是类能够被序列化的特性,表示这个类能够被序列化。
什么叫序列化?
我们都知道对象是临时保存在内存中的,不能用U盘考走了,有时为了使用介质转移对象,而且把对象的状态保持下来,就须要把对象保存下来,这个过程就叫做序列化,通俗点,就是把人的魂(对象)收伏成一个石子(可传输的介质)
什么叫反序列化?
就是再把介质中的东西还原成对象,把石子还原成人的过程。
在进行这些操作的时候都须要这个能够被序列化,要能被序列化,就得给类头加[Serializable]特性。
通常网络程序为了传输安全才这么做。
简单介绍
序列化是指将对象实例的状态存储到存储媒体的过程。在此过程中,先将对象的公共字段和私有字段以及类的名称(包含类所在的程序集)转换为字节流,然后再把字节流写入数据流。在随后对对象进行反序列化时,将创建出与原对象全然同样的副本。
在面向对象的环境中实现序列化机制时,必须在易用性和灵活性之间进行一些权衡。仅仅要您对此过程有足够的控制能力,就能够使该过程在非常大程度上自己主动进行。比如,简单的二进制序列化不能满足须要,或者,因为特定原因须要确定类中那些字段须要序列化。下面各部分将探讨 .NET 框架提供的可靠的序列化机制,并着重介绍使您能够依据须要自己定义序列化过程的一些重要功能。
持久存储
我们常常须要将对象的字段值保存到磁盘中,并在以后检索此数据。虽然不使用序列化也能完毕这项工作,但这样的方法通常非常繁琐并且easy出错,并且在须要跟踪对象的层次结构时,会变得越来越复杂。能够想象一下编写包括大量对象的大型业务应用程序的情形,程序猿不得不为每个对象编写代码,以便将字段和属性保存至磁盘以及从磁盘还原这些字段和属性。序列化提供了轻松实现这个目标的快捷方法。
公共语言执行时 (CLR) 管理对象在内存中的分布,.NET 框架则通过使用反射提供自己主动的序列化机制。对象序列化后,类的名称、程序集以及类实例的全部数据成员均被写入存储媒体中。对象通经常使用成员变量来存储对其它实例的引用。类序列化后,序列化引擎将跟踪全部已序列化的引用对象,以确保同一对象不被序列化多次。.NET 框架所提供的序列化体系结构能够自己主动正确处理对象图表和循环引用。对对象图表的唯一要求是,由正在进行序列化的对象所引用的全部对象都必须标记为 Serializable(请參阅基本序列化)。否则,当序列化程序试图序列化未标记的对象时将会出现异常。
当反序列化已序列化的类时,将又一次创建该类,并自己主动还原全部数据成员的值。
按值封送
对象仅在创建对象的应用程序域中有效。除非对象是从 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();
本例使用二进制格式化程序进行序列化。您仅仅需创建一个要使用的流和格式化程序的实例,然后调用格式化程序的 Serialize 方法。流和要序列化的对象实例作为參数提供给此调用。类中的全部成员变量(甚至标记为 private 的变量)都将被序列化,但这一点在本例中未明白体现出来。在这一点上,二进制序列化不同于仅仅序列化公共字段的 XML 序列化程序。
将对象还原到它曾经的状态也很easy。首先,创建格式化程序和流以进行读取,然后让格式化程序对对象进行反序列化。下面代码片段说明了怎样进行此操作。
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Open,
FileAccess.Read, FileShare.Read);
MyObject obj = (MyObject) formatter.Deserialize(fromStream);
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 调用不变。对于上面使用的演示样例,该格式化程序将生成下面结果。
<SOAP-ENV:Envelope
xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP- ENC=http://schemas.xmlsoap.org/soap/encoding/
xmlns:SOAP- ENV=http://schemas.xmlsoap.org/soap/envelope/
SOAP-ENV:encodingStyle=
"http://schemas.microsoft.com/soap/encoding/clr/1.0
http://schemas.xmlsoap.org/soap/encoding/"
xmlns:a1="http://schemas.microsoft.com/clr/assem/ToFile">
<SOAP-ENV:Body>
<a1:MyObject id="ref-1">
<n1>1</n1>
<n2>24</n2>
<str id="ref-3">一些字符串</str>
</a1:MyObject>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
须要注意的是,无法继承 Serializable 属性。假设从 MyObject 派生出一个新的类,则这个新的类也必须使用该属性进行标记,否则将无法序列化。比如,假设试图序列化下面类实例,将会显示一个 SerializationException,说明 MyStuff 类型未标记为可序列化。
public class MyStuff : MyObject
{
public int n3;
}
使用序列化属性很方便,可是它存在上述的一些限制。有关何时标记类以进行序列化(由于类编译后就无法再序列化),请參考有关说明(请參阅以下的序列化规则)。
选择性序列化
类通常包括不应被序列化的字段。比如,如果某个类用一个成员变量来存储线程 ID。当此类被反序列化时,序列化此类时所存储的 ID 相应的线程可能不再执行,所以对这个值进行序列化没有意义。能够通过使用 NonSerialized 属性标记成员变量来防止它们被序列化,例如以下所看到的:
[Serializable]
public class MyObject
{
public int n1;
[NonSerialized] public int n2;
public String str;
}
自己定义序列化
能够通过在对象上实现 ISerializable 接口来自己定义序列化过程。这一功能在反序列化后成员变量的值失效时尤事实上用,可是须要为变量提供值以重建对象的完整状态。要实现 ISerializable,须要实现 GetObjectData 方法以及一个特殊的构造函数,在反序列化对象时要用到此构造函数。下面代码演示样例说明了怎样在前一部分中提到的 MyObject 类上实现 ISerializable。
[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 方法。
须要强调的是,将 ISerializable 加入至某个类时,须要同一时候实现 GetObjectData 以及特殊的构造函数。假设缺少 GetObjectData,编译器将发出警告。可是,因为无法强制实现构造函数,所以,缺少构造函数时不会发出警告。假设在没有构造函数的情况下尝试反序列化某个类,将会出现异常。在消除潜在安全性和版本号控制问题等方面,当前设计优于 SetObjectData 方法。比如,假设将 SetObjectData 方法定义为某个接口的一部分,则此方法必须是公共方法,这使得用户不得不编写代码来防止多次调用 SetObjectData 方法。能够想象,假设某个对象正在运行某些操作,而某个恶意应用程序却调用此对象的 SetObjectData 方法,将会引起一些潜在的麻烦。
在反序列化过程中,使用出于此目的而提供的构造函数将 SerializationInfo 传递给类。对象反序列化时,对构造函数的不论什么可见性约束都将被忽略,因此,能够将类标记为 public、protected、internal 或 private。一个不错的办法是,在类未封装的情况下,将构造函数标记为 protect。假设类已封装,则应标记为 private。要还原对象的状态,仅仅需使用序列化时採用的名称,从 SerializationInfo 中检索变量的值。假设基类实现了 ISerializable,则应调用基类的构造函数,以使基础对象能够还原其变量。
假设从实现了 ISerializable 的类派生出一个新的类,则仅仅要新的类中含有不论什么须要序列化的变量,就必须同一时候实现构造函数以及 GetObjectData 方法。下面代码片段显示了怎样使用上文所看到的的 MyObject 类来完毕此操作。
[Serializable]
public class ObjectTwo : MyObject
{
public int num;
public ObjectTwo() : base()
{
}
protected ObjectTwo(SerializationInfo si, StreamingContext context) :
base(si,context)
{
num = si.GetInt32("num");
}
public override void GetObjectData(SerializationInfo si,
StreamingContext context)
{
base.GetObjectData(si,context);
si.AddValue("num", num);
}
}
切记要在反序列化构造函数中调用基类,否则,将永远不会调用基类上的构造函数,而且在反序列化后也无法构建完整的对象。
对象被彻底又一次构建,可是在反系列化过程中调用方法可能会带来不良的副作用,由于被调用的方法可能引用了在调用时尚未反序列化的对象引用。假设正在进行反序列化的类实现了 IDeserializationCallback,则反序列化整个对象图表后,将自己主动调用 OnSerialization 方法。此时,引用的全部子对象均已全然还原。有些类不使用上述事件侦听器,非常难对它们进行反序列化,散列表便是一个典型的样例。在反序列化过程中检索keyword/值对非常easy,可是,由于无法保证从散列表派生出的类已反序列化,所以把这些对象加入回散列表时会出现一些问题。因此,建议眼下不要在散列表上调用方法。
序列化过程的步骤
在格式化程序上调用 Serialize 方法时,对象序列化依照下面规则进行:
检查格式化程序是否有代理选取器。假设有,检查代理选取器是否处理指定类型的对象。假设选取器处理此对象类型,将在代理选取器上调用 ISerializable.GetObjectData。
假设没有代理选取器或有却不处理此类型,将检查是否使用 Serializable 属性对对象进行标记。假设未标记,将会引发 SerializationException。
假设对象已被正确标记,将检查对象是否实现了 ISerializable。假设已实现,将在对象上调用 GetObjectData。
假设对象未实现 Serializable,将使用默认的序列化策略,对全部未标记为 NonSerialized 的字段都进行序列化。
版本号控制
.NET 框架支持版本号控制和并排运行,而且,假设类的接口保持一致,全部类均可跨版本号工作。因为序列化涉及的是成员变量而非接口,所以,在向要跨版本号序列化的类中加入成员变量,或从中删除变量时,应慎重行事。特别是对于未实现 ISerializable 的类更应如此。若当前版本号的状态发生了不论什么变化(比如加入成员变量、更改变量类型或更改变量名称),都意味着假设同一类型的现有对象是使用早期版本号进行序列化的,则无法成功对它们进行反序列化。
假设对象的状态须要在不同版本号间发生改变,类的作者能够有两种选择:
实现 ISerializable。这使您能够精确地控制序列化和反序列化过程,在反序列化过程中正确地加入和解释未来状态。
使用 NonSerialized 属性标记不重要的成员变量。仅当估计类在不同版本号间的变化较小时,才可使用这个选项。比如,把一个新变量加入至类的较高版本号后,能够将该变量标记为 NonSerialized,以确保该类与早期版本号保持兼容。
序列化规则
因为类编译后便无法序列化,所以在设计新类时应考虑序列化。须要考虑的问题有:是否必须跨应用程序域来发送此类?是否要远程使用此类?用户将怎样使用此类?或许他们会从我的类中派生出一个须要序列化的新类。仅仅要有这样的可能性,就应将类标记为可序列化。除下列情况以外,最好将全部类都标记为可序列化:
全部的类都永远也不会跨越应用程序域。假设某个类不要求序列化但须要跨越应用程序域,请从 MarshalByRefObject 派生此类。
类存储仅适用于其当前实例的特殊指针。比如,假设某个类包括非受控的内存或文件句柄,请确保将这些字段标记为 NonSerialized 或根本不序列化此类。
某些数据成员包括敏感信息。在这样的情况下,建议实现 ISerializable 并仅序列化所要求的字段。