C#特性(Attribute)之Serializable特性
本⽂参考⾃,纯属读书笔记,加深记忆
介绍之前,先说⼀个重要的知识点:
Serializable属性并不序列化类,它只是⼀个标签。⾄于如何序列化,各种序列化类各⾃有各⾃的做法,它们只是读取这个标签⽽已,之后就按照⾃⼰的⽅式去序列化,例如某个应⽤程序会反射⽬标对象的类型的所有Field和Property,看看它是否实现了ISerializable,如果实现了就调⽤它。你可以看看关于实现ISerializable接⼝来使⾃⼰的类可以被序列化的做法。某些应⽤程序查SerializableAttribute属性来避免去花时间反射对象的属性,当它看到⼀个对象没有标记为可序列化,就会直接报错,⽽不会去花时间反射字段。
但是像WCF,即使你不去标记,它也会花时间去反射字段。它已经不在乎SerializableAttribute了。
SerializableAttribute仅是⼀个标记⽽已,它并不执⾏序列化动作。
1、Serializable特性的作⽤
序列化的attribute,是为了利⽤序列化的技术准备⽤于序列化的对象必须设置 [System.Serializable] 标签,
该标签指⽰⼀个类可以序列化。便于在⽹络中传输和保存这个标签是类可以被序列化的特性,表⽰这个类可以被序列化。
什么叫序列化?
我们都知道对象是暂时保存在内存中的,不能⽤U盘考⾛了,有时为了使⽤介质转移对象,并且把对象的状态保持下来,就需要把对象保存下来,这个过程就叫做序列化,通俗点,就是把⼈的魂(对象)收伏成⼀个⽯⼦(可传输的介质)
什么叫反序列化?
就是再把介质中的东西还原成对象,把⽯⼦还原成⼈的过程。
在进⾏这些操作的时候都需要这个可以被序列化,要能被序列化,就得给类头加[Serializable]特性。
通常⽹络程序为了传输安全才这么做。
2、关于序列化和反序列化的执⾏过程和原理
持久存储
我们经常需要将对象的字段值保存到磁盘中,并在以后检索此数据。尽管不使⽤序列化也能完成这项⼯作,但这种⽅法通常很繁琐⽽且容易出错,并且在需要跟踪对象的层次结构时,会变得越来越复杂。可以想象⼀下编写包含⼤量对象的⼤型业务应⽤程序的情形,程序员不得不为每⼀个对象编写代码,以便将字段和属性保存⾄磁盘以及从磁盘还原这些字段和属性。序列化提供了轻松实现这个⽬标的快捷⽅法。
公共语⾔运⾏时 (CLR) 管理对象在内存中的分布,.NET 框架则通过使⽤反射提供⾃动的序列化机制。对象序列化后,类的名称、程序集以及类实例的所有数据成员均被写⼊存储媒体中。对象通常⽤成员变量来存储对其他实例的引⽤。类序列化后,序列化引擎将跟踪所有已序列化的引⽤对象,以确保同⼀对象不被序列化多次。.NET 框架所提供的序列化体系结构可以⾃动正确处理对象图表和循环引⽤。对对象图表的唯⼀要求是,由正在进⾏序列化的对象所引⽤的所有对象都必须标记为 Serializable(请参阅基本序列化)。否则,当序列化程序试图序列化未标记的对象时将会出现异常。
当反序列化已序列化的类时,将重新创建该类,并⾃动还原所有数据成员的值。
按值封送对象仅在创建对象的应⽤程序域中有效。除⾮对象是从 MarshalByRefObject 派⽣得到或标记为 Serializable,否则,任何将对象作为参数传递或将其作为结果返回的尝试都将失败。如果对象标记为 Serializable,则该对象将被⾃动序列化,并从⼀个应⽤程序域传输⾄另⼀个应⽤程序域,然后进⾏反序列化,从⽽在第⼆个应⽤程序域中产⽣出该对象的⼀个精确副本。此过程通常称为按值封送。
如果对象是从 MarshalByRefObject 派⽣得到,则从⼀个应⽤程序域传递⾄另⼀个应⽤程序域的是对象引⽤,⽽不是对象本⾝。也可以将从MarshalByRefObject 派⽣得到的对象标记为 Serializable。远程使⽤此对象时,负责进⾏序列化并已预先配置为 SurrogateSelector 的格式化程序将控制序列化过程,并⽤⼀个代理替换所有从 MarshalByRefObject 派⽣得到的对象。如果没有预先配置为 SurrogateSelector,序列化体系结构将遵从下⾯的标准序列化规则(请参阅序列化过程的步骤)。
3、基本序列化例⼦
(1)、⾸先需要⼀个可序列化的类,代码如下:
[Serializable]
public class MyObject
{
public int n1 { get; set; }
public int n2 { get; set; }
writeline方法的作用public string str { get; set; }
}
通过添加[Serializable]特性确保当前类可以被实例化。
(2)、将类实例序列化进流,代码如下:
public static void SerializableObj()
{
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 序列化程序
(3)、将对应的类实例进⾏反序列化
public static MyObject DeSerializableObj()
{
IFormatter formatter = new BinaryFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Open,
FileAccess.Read, FileShare.Read);
MyObject obj = (MyObject)formatter.Deserialize(stream);
stream.Close();
return obj;
}
(4)、测试过程是否成功
static void Main(string[] args){
SerializableObj();
MyObject obj = DeSerializableObj();
Console.WriteLine("n1: {0}", obj.n1);
Console.WriteLine("n2: {0}", obj.n2);
Console.WriteLine("str: {0}", obj.str);
}
输出:
说明整个序列化和反序列化的过程成功!!!注意:需要序列化的类必须将[Serializable]特性,否则会报错!!!
上⾯所使⽤的 BinaryFormatter 效率很⾼,能⽣成⾮常紧凑的字节流。所有使⽤此格式化程序序列化的对象也可使⽤它进⾏反序列化,对于序列化将在 .NET 平台上进⾏反序列化的对象,此格式化程序⽆疑是⼀个理想⼯具。需要注意的是,对对象进⾏反序列化时并不调⽤构造函数。对反序列化添加这项约束,是出于性能⽅⾯的考虑。但是,这违反了对象编写者通常采⽤的⼀些运⾏时约定,因此,开发⼈员在将对象标记为可序列化时,应确保考虑了这⼀特殊约定。
4、序列化对象的可移植性
如果要求序列化完之后的对象,具有可移植性,就是你希望在.Net平台下序列化的实体类对象,在Java平台下也能适⽤。
那就⽤SoapFormatter,⽽ Serialize 和 Deserialize 调⽤不变,代码如下:
public static void SerializableObj()
{
MyObject obj = new MyObject();
obj.n1 = 1;
obj.n2 = 24;
obj.str = "⼀些字符串";
SoapFormatter formatter = new SoapFormatter();
Stream stream = new FileStream("MyFile.bin", FileMode.Create,
FileAccess.Write, FileShare.None);
stream.Close();
}
序列化结果如下:
<SOAP-ENV:Envelope
xmlns:xsi=/2001/XMLSchema-instance
xmlns:xsd="/2001/XMLSchema"
xmlns:SOAP- ENC=/soap/encoding/
xmlns:SOAP- ENV=/soap/envelope/
SOAP-ENV:encodingStyle=
"schemas.microsoft/soap/encoding/clr/1.0
/soap/encoding/"
xmlns:a1="schemas.microsoft/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;
}
使⽤序列化属性⾮常⽅便,但是它存在上述的⼀些限制。有关何时标记类以进⾏序列化(因为类编译后就⽆法再序列化),请参考有关说明(请参阅下⾯的序列化规则)。
5、选择性序列化
类通常包含不应被序列化的字段。例如,假设某个类⽤⼀个成员变量来存储线程 ID。当此类被反序列化时,序列化此类时所存储的 ID 对应的线程可能不再运⾏,所以对这个值进⾏序列化没有意义。可以通过使⽤ NonSerialized 属性标记成员变量来防⽌它们被序列化,
[Serializable]
public class MyObject
{
public int n1;
[NonSerialized] public int n2;
public String str;
}
6、⾃定义序列化
可以通过在对象上实现 ISerializable 接⼝来⾃定义序列化过程。这⼀功能在反序列化后成员变量的值失效时尤其有⽤,但是需要为变量提供值以重建对象的完整状态。要实现 ISerializable,需要实现 GetObjectData ⽅法以及⼀个特殊的构造函数,在反序列化对象时要⽤到此构造函数。以下代码⽰例说明了如何在前⼀部分中提到的 MyObject 类上实现 ISerializable。
[Serializable]
public class MyObject : ISerializable
{
public int n11;
public int n22;
public String str1;
public MyObject()
{
}
protected MyObject(SerializationInfo info, StreamingContext context)
{
n1 = info.GetInt32("i");
n2 = info.GetInt32("j");
str = info.GetString("k");
}
public 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,则应调⽤基类的构造函数,以使基础对象可以还原其变量。
7、从实现了 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 ⽅法。此时,引⽤的所有⼦对象均已完全还原。有些类不使⽤上述事件侦听器,很难对它们进⾏反序列化,散列表便是⼀个典型的例⼦。在反序列化过程中检索关键字/值对⾮常容易,但是,由于⽆法保证从散列表派⽣出的类已反序列化,所以把这些对象添加回散列表时会出现⼀些问题。因此,建议⽬前不要在散列表上调⽤⽅法。
8、序列化过程的步骤
在格式化程序上调⽤ Serialize ⽅法时,对象序列化按照以下规则进⾏:
检查格式化程序是否有代理选取器。如果有,检查代理选取器是否处理指定类型的对象。如果选取器处理此对象类型,将在代理选取器上调⽤ ISerializable.GetObjectData。
如果没有代理选取器或有却不处理此类型,将检查是否使⽤ Serializable 属性对对象进⾏标记。如果未标记,将会引发SerializationException。
如果对象已被正确标记,将检查对象是否实现了 ISerializable。如果已实现,将在对象上调⽤ GetObjectData。
如果对象未实现 Serializable,将使⽤默认的序列化策略,对所有未标记为 NonSerialized 的字段都进⾏序列化。
版本控制
.NET 框架⽀持版本控制和并排执⾏,并且,如果类的接⼝保持⼀致,所有类均可跨版本⼯作。由于序列化涉及的是成员变量⽽⾮接⼝,所以,在向要跨版本序列化的类中添加成员变量,或从中删除变量时,应谨慎⾏事。特别是对于未实现 ISerializable 的类更应如此。若当前版本的状态发⽣了任何变化(例如添加成员变量、更改变量类型或更改变量名称),都意味着如果同⼀类型的现有对象是使⽤早期版本进⾏序列化的,则⽆法成功对它们进⾏反序列化。
如果对象的状态需要在不同版本间发⽣改变,类的作者可以有两种选择:
实现 ISerializable。这使您可以精确地控制序列化和反序列化过程,在反序列化过程中正确地添加和解释未来状态。
使⽤ NonSerialized 属性标记不重要的成员变量。仅当预计类在不同版本间的变化较⼩时,才可使⽤这个选项。例如,把⼀个新变量添加⾄类的较⾼版本后,可以将该变量标记为 NonSerialized,以确保该类与早期版本保持兼容。
序列化规则
由于类编译后便⽆法序列化,所以在设计新类时应考虑序列化。需要考虑的问题有:是否必须跨应⽤程序域来发送此类?是否要远程使⽤此类?⽤户将如何使⽤此类?也许他们会从我的类中派⽣出⼀个需要序列化的新类。只要有这种可能性,就应将类标记为可序列化。除下列情
况以外,最好将所有类都标记为可序列化:
所有的类都永远也不会跨越应⽤程序域。如果某个类不要求序列化但需要跨越应⽤程序域,请从 MarshalByRefObject 派⽣此类。
类存储仅适⽤于其当前实例的特殊指针。例如,如果某个类包含⾮受控的内存或⽂件句柄,请确保将这些字段标记为 NonSerialized 或根本不序列化此类。
某些数据成员包含敏感信息。在这种情况下,建议实现 ISerializable 并仅序列化所要求的字段。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论