【C#】详解C#序列化
目录结构:
在这边文章中,笔者将会将会详细阐述C#中的序列化和反序列,希望可以对你有所帮助。
1.简介
众所周知,序列化是将对象或对象图转化字节流的过程,反序列化是将字节流转化为对象图的过程。
如果要使一个类型可序列化的话,必需向类型应用定制特性System.SerializableAttribute。请注意,SerializableAttribute特性只能应用于引用类型(class)、值类型(struct)、枚举类型(enum)和委托类型(delegate)。枚举和委托类型总是可序列化的所以不必显示使用SerializableAttribute特性。
序列化必须要使用到序列化器,它用于完成将数据转化为特定格式的数据。以下列举四种格式化器:
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter 以二进制的格式对对象进行序列化和反序列操作。
System.Runtime.Serialization.Formatters.Soap.SoapFormatter 以SOAP的格式对对象进行序列化和反序列化操作,从.NET Framework2.0开始,该类就废弃,推荐使用BinaryFormatter类。
System.Runtime.Serialization.NetDataContractSerializer 用.NET Framework提供的类型,将类型实例序列化和返序列化为XML流或文档结构。
System.Runtime.Serialization.DataContractSerializer 使用指定的数据协定,将类型实例序列化和反序列化XML流或文档结构
System.Xml.Serialization.XmlSerializer 将类型的实例序列化和反序列化XML文档,该类允许控制如何将对象编码为XML文档。
2.控制序列化和反序列化
如果将SerializableAttribute特性应用于某个类型,那么标志该类型的实例可以进行序列化和反序列化操作,该类型实例的所有数据都可以进行序列化和反序列化操作,如果需要更精准的控制序列化和反序列化的数据,那么就需要控制序列化和反序列化的过程了。
这里笔者把序列化和反序列化的操作方式分为两种,分别为通过特性和通过接口的方式。
2.1 特性(OnSerializing、OnSerialized、OnDeserializing、OnDeserialized、NonSerialized...)
在这里笔者将会介绍序列化中常用的特性,用这些特性可以控制序列化的过程。
System.Runtime.Serialization.OnSerializingAttribute:
应用OnSerializingAttribute特性的方法,将会在序列化期间被调用。同时,应用OnSerializingAttribute特性的方法必须包含一个System.Runtime.Serialization.StreamingContext参数。
System.Runtime.Serialization.OnSerializedAttribute:
应用OnSerializedAttribute特性的方法,将会在序列化之后被调用。同时,应用OnSerializedAttribute特性的方法必须包含一个System.Runtime.Serialization.StreamingContext参数。
System.Runtime.Serialization.OnDeserializingAttribute:
应用OnDeserializingAttribute特性的方法,将会在被序列化期间被调用。同时,应用OnDeserializingAttribute特性的方法必须包含一个System.Runtime.Serialization.StreamingContext参数。
System.Runtime.Serialization.OnDeserializedAttribute:
应用OnDeserializedAttribute特性的方法,将会在被序列化之后调用。同时,应用OnDeserializedAttribute特性的方法必须包含一个System.Runtime.Serialization.StreamingContext参数。
System.NonSerializedAttribute:
应用NonSerializedAttribute特性的字段,将会不会序列化。可以利用这个特性保护保护敏感数据,NonSerializedAttribute不仅可以引用字段,还以应用于event。
例如:
[field:NonSerializedAttribute()] public event ChangedEventHandler Changed;
下面给上面使用上面特性的类:
//标记为可序列化 [Serializable] class MyType { Int32 x, y; //标记num为不可序列 [NonSerialized] Int32 num; public MyType(Int32 x, Int32 y) { this.x = x; this.y = y; this.num=(x+y); } //标记该方法在序列化期间被调用 [OnSerializing] private void OnSerializing(StreamingContext context) { //举例:在序列化前,修改任何需要修改的状态 } //标记该方法在序列化之后被调用 [OnSerialized] private void OnSerialized(StreamingContext context) { //举例:在序列化之后,恢复任何需要恢复的状态 } //标记该方法在反序列化期间被调用 [OnDeserializing] private void OnDeserialing(StreamingContext context) { //举例:在反序列化期间,为字段设置默认值 } //标记该方法在反序列化之后被调用 [OnDeserialized] private void OnDeserialized(StreamingContext context) { //举例:根据字段值初始化瞬间状态(比如num值) num = x + y; } }
2.2 接口(ISerializable)
在前面已经介绍过通过OnSerializing,OnSerialized,OnDeserializing,OnDeserialized等特性。除了使用特性,还可以让类型实现System.Runtime.Serialization.ISerializable接口。
该接口的定义如下:
public interface ISerializable{ void GetObjectData(SerializationInfo info,StreamingContext context); }
实现ISerializable接口,除了需要实现GetObjectData方法,还应该提供一个特殊的构造器。
注意:
ISerializable接口和特殊构造器旨在由格式化器使用,但其他代码可能调用GetObjectData来返回敏感数据,或传入损坏的数据。建议向GetObjectData方法和特殊构造器应用以下特性:
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter=true)]
如果在类型中必须访问提取对象中的成员,建议类型提供一个OnDeserialized特性或是实现IDeseializationCallback接口的OnDeserialization方法。调用该方法时,所有对象的字段都已经初始化好了。
class Program { static void Main(string[] args) { MyItemType myItermType = new MyItemType("hello"); using(MemoryStream memoryStream = new MemoryStream()){ BinaryFormatter binaryFormatter = new BinaryFormatter(); binaryFormatter.Serialize(memoryStream, myItermType); memoryStream.Position = 0; myItermType = null; myItermType = (MyItemType)binaryFormatter.Deserialize(memoryStream); Console.WriteLine(myItermType.MyProperty);//hello } Console.ReadLine(); } } [Serializable] public class MyItemType : ISerializable,IDeserializationCallback { private string myProperty_value; [NonSerialized] private SerializationInfo m_info = null; public MyItemType(String property) { this.myProperty_value = property; } public string MyProperty { get { return myProperty_value; } set { myProperty_value = value; } } //在序列化期间被调用 public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("props", myProperty_value, typeof(string)); } //在反序列化期间被调用 public MyItemType(SerializationInfo info, StreamingContext context) { //将SerializationInfo的引用保留下来。 //之所以不在构造方法中完成字段赋值,是因为如果要访问当前对象的成员(方法),那么此时成员很有可能没有初始化完成,可能出现不可预期的结果 m_info = info; } //在反序列化之后调用 public void OnDeserialization(object sender) { myProperty_value = (string)m_info.GetValue("props", typeof(string)); } }
在这里,在上面我们知道了SerializationInfo对象其中一个重要的方法就是AddValue,使用该方法可以将对象添加到序列化的过程中。SerializationInfo除了AddValue,还有一个值得说明的方法就是setType,使用这个方法可以设置序列化的数据类型,如果恰好该类型实现了IObjectReference接口的话,将会在反序列化之后,自动调用其抽象方法:
IObjectReference接口原型为:
public interface IObjectReference{ Object GetRealObject(StreamingContext context); }
看如下如何序列化和反序列化单实例的栗子:
[Serializable] public sealed class Singleton : ISerializable { //该类型的实例 private static readonly Singleton s_theOneObject = new Singleton(); //实例字段 public String Name = "Jeff"; public DateTime Date = DateTime.Now; //私有构造器,只允许这个类型构造单实例 private Singleton() { } //返回对该单实例的引用 public static Singleton GetSingleton() { return s_theOneObject; } //序列化一个Singleton时调用 [SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)] void ISerializable.GetObjectData(SerializationInfo info,StreamingContext context) { info.SetType(typeof(SingletonSerializationHelper)); } [Serializable] private sealed class SingletonSerializationHelper : IObjectReference { //该方法在对象反序列化之后调用 public Object GetRealObject(StreamingContext context) { return Singleton.GetSingleton(); } } //注意:特殊构造器不需要,因为它永远都不会被调用 }
测试代码为:
static void Main(string[] args) { Singleton[] a1 = { Singleton.GetSingleton(),Singleton.GetSingleton()}; Console.WriteLine(a1[0]==a1[1]);//true using(var stream=new MemoryStream()){ BinaryFormatter formatter = new BinaryFormatter(); //先序列化再反序列化 formatter.Serialize(stream,a1); stream.Position = 0; Singleton[] a2 = (Singleton[])formatter.Deserialize(stream); Console.WriteLine(a2[0]==a2[1]);//true Console.WriteLine(a1[0] == a1[1]);//true; } Console.ReadLine(); }
3.流上下文(StreamingContext)
一组序列化好的对象有许多用处;同一个进程,同一台机器上的不同进程、不同机器上的不同进程。在一些比较少见的情况下,一个对象可能想知道它要在什么地方被反序列化,从而以不同的方式生成其形态。例如,如果对象中包装了Windows信号量(semaphone)对象,如果它知道要反序列化到同一个进程中,就可决定对它的内核句柄(kernal handle)进行序列化,这是因为内核句柄在同一个进程中有限。但如果要反序列化到同一台机器的不同进程中,那么可以决定对信号量的字符串名称序列化。最后,如果要反序列化到不同机器上,那么就可决定抛出异常,因为信号量只在同一台机器上有效。
SteamingContext有两个公共只读属性,如下所示:
Sate | StreamingContextStates | 一组位标志,指定要序列化/反序列化的对象的来源或目的地 |
Context | Object | 一个对象引用,对象中包含用户希望的任何上下文信息 |
通过State属性,就可判断序列化/反序列化的来源或目的地。
StreamingContextStates的标志:
标志说明 | 标志值 | 说明 |
CrossProcess | 0x0001 | 来源或目的地是同一台机器的不同进程 |
CrossMachines | 0x0002 | 来源或目的地在不同机器上 |
File | 0x0004 | 来源或目的地是文件。不保证反序列化数据是同一个进程 |
Persistence | 0x0008 | 来源或目的地是存储(store),比如数据库或文件。不保证反序列化数据的是同一个进程 |
Remoting | 0x0010 | 来源或目的地是远程的未知未知。这个位置可能在(也可能不在)同一台机器上。 |
Other | 0x0020 | 来源或目的地未知 |
Clone | 0x0040 | 对象图被克隆。序列化代码可认为是由同一进程对数据进行反序列化,所以可安全地访问句柄或其他非托管设备。 |
CrossAppDomain | 0x0080 | 来源或目的地是不通过的AppDomain |
All | 0x00FF | 来源或目的地可能是上述任何一个上下文。这是默认设定 |
知道了如何获取这些信息后,接下来进行设置这些信息。在IFormatter接口(BinaryFormatter和SoapFormtter类型均实现该接口)定义了StreamingContext的可读/可写属性Context,构造格式化器时候,格式化器会初始化它的Context属性,将StreamingContextStates状态设置为All,将其对额外状态对象的引用设置为null。
接下来举如下栗子:
private static Object DeepClone(Object original) { using(MemoryStream stream=new MemoryStream()){ BinaryFormatter formatter = new BinaryFormatter(); formatter.Context = new StreamingContext(StreamingContextStates.Clone); formatter.Serialize(stream,original); //定义到内存流的起始位置 stream.Position = 0; return formatter.Deserialize(stream); } }
4.序列化代理
前面介绍了如何修改一个类型的实现,控制该类型如何对它本身的实例进行序列化和反序列化。然而,格式化器还允许不是“类型实现的一部分”的代码重写该类型“序列化和反序列化其对象”。这就是序列化代理。
要使这个机子工作起来,需要按照如下步骤:
a.首先要定义一个“代理类型”,它接管对现有类型的序列化和反序列化活动
b.向格式化器登记注册这个代理类型的实例,并告诉格式化器代理要作为的类型是什么。
c.一旦格式化器序列化/反序列化这个类型,那么将会调用由关联的代理类型关联的方法。
代理类型必须实现System.Runtime.Serialization.ISerializationSurrogate接口,该接口定义如下:
public interface ISerializationSurrogate{ void GetObjectDate(Object obj,SerializationInfo info,StreamingContext context); Object SetObjectDate(Object obj,SerializationInfo info,StreamingContext context,ISurrogateSelector selector); }
其中GetObjectDate在序列化时调用,SetObjectDate在反序列化时调用。
下面的栗子展示了如何使用代理类:
internal sealed class UniversalToLocalTimeSerializationSurrogate : ISerializationSurrogate { public void GetObjectData(object obj,SerializationInfo info,StreamingContext context) { //将DateTime从本地时间转化为UTC info.AddValue("date", ((DateTime)obj).ToUniversalTime().ToString("u")); } public object SetObjectData(object obj,SerializationInfo info,StreamingContext context,ISurrogateSelector selector) { //将Datetime从UTC转化为本地时间 return DateTime.ParseExact(info.GetString("date"),"u",null).ToLocalTime(); } }
测试代码如下:
static void Main(string[] args) { using(var stream=new MemoryStream()){ //构造格式化器 IFormatter formatter = new BinaryFormatter(); //构造SurrogateSelector(代理选择器)对象 SurrogateSelector ss = new SurrogateSelector(); SurrogateSelector ss2 = new SurrogateSelector(); ss.ChainSelector(ss2); //告诉代理选择器为DateTime对象使用指定的代理对象 ss.AddSurrogate(typeof(DateTime),formatter.Context,new UniversalToLocalTimeSerializationSurrogate()); //注意:addSurrogate可多次调用来登记代理 //告诉格式化器使用代理选择器 formatter.SurrogateSelector = ss; //创建一个DateTime对象代表本地时间,并序列化它 DateTime localTimeBeforeSerialize = DateTime.Now; formatter.Serialize(stream,localTimeBeforeSerialize); //stream将Universal时间作为一个字符串显示,证明序列化成功 stream.Position = 0; Console.WriteLine(new StreamReader(stream).ReadToEnd()); //反序列化Universal字符串 stream.Position = 0; DateTime localTimeAfterDeserialize = (DateTime)formatter.Deserialize(stream); Console.WriteLine("DateTimeBeforSerialize={0}", localTimeBeforeSerialize);//DateTimeAfterSerialize=2018/8/26 16:42:14 Console.WriteLine("DateTimeAfterSerialize={0}", localTimeAfterDeserialize);//DateTimeAfterSerialize=2018/8/26 16:42:14 Console.ReadLine(); } }