一、什么是运行时序列化
序列化的作用就是将对象图(特定时间点的对象连接图)转换为字节流,这样这些对象图就可以在文件系统/网络进行传输。
二、序列化/反序列化快速入门
一般来说我们通过 FCL 提供的 BinaryFormatter 对象就可以将一个对象序列化为字节流进行存储,或者通过该 Formatter 
将一个字节流反序列化为一个对象。
FCL 的序列化与反序列化
序列化操作:
public MemoryStream SerializeObj(object sourceObj) { var memStream = new 
MemoryStream(); var formatter = new BinaryFormatter(); 
formatter.Serialize(memStream, sourceObj); return memStream; } 
反序列化操作:
public object DeserializeFromStream(MemoryStream stream) { var formatter = new 
BinaryFormatter(); stream.Position = 0; return formatter.Deserialize(stream); } 
反序列化通过 Formatter 的 Deserialize() 方法返回序列化好的对象图的根对象的一个引用。
深拷贝
通过序列化与反序列化的特性,可以实现一个深拷贝的方法,用户创建源对象的一个克隆体。
public object DeepClone(object originalObj) { using (var memoryStream = new 
MemoryStream()) { var formatter = new BinaryFormatter(); 
formatter.Serialize(memoryStream, originalObj); // 表明对象是被克隆的,可以安全的访问其他托管资源 
formatter.Context = new StreamingContext(StreamingContextStates.Clone); 
memoryStream.Position = 0; return formatter.Deserialize(memoryStream); } } 
另外一种技巧就是可以将多个对象图序列化到一个流当中,即调用多次 Serialize() 
方法将多个对象图序列化到流当中。如果需要反序列化的时候,按照序列化时对象图的序列化顺序反向反序列化即可。
BinaryFormatter 在序列化的时候会将类型的全名与程序集定义写入到流当中,这样在反序列化的时候,格式化器会获取这些信息,并且通过 
System.Reflection.Assembly.Load() 方法将程序集加载到当前的 AppDomain。
在程序集加载完成之后,会在该程序集搜索待反序列化的对象图类型,找不到则会抛出异常。
【注意】
某些应用程序通过 Assembly.LoadFrom() 
来加载程序集,然后根据程序集中的类型来构造对象。序列化该对象是没问题的,但是反序列化的时候格式化器使用的是Assembly.Load() 
方法来加载程序集,这样的话就会导致无法正确加载对象。
这个时候,你可以实现一个与 System.ResolveEventHandler 签名一样的委托,并且在反序列化注册到当前 AppDomain 的 
AssemblyResolve 事件。
这样当程序集加载失败的时候,你可以在该方法内部根据传入的事件参数与程序集标识自己使用 Assembly.LoadFrom() 来构造一个 Assembly 
对象。
记得在反序列化完成之后,马上向事件注销这个方法,否则会造成内存泄漏。
三、使类型可序列化
在设计自定义类型时,你需要显式地通过 Serializable 
特性来声明你的类型是可以被序列化的。如果没有这么做,在使用格式化器进行序列化的时候,则会抛出异常。
[Serializable] public class DIYClass { public int x { get; set; } public int y 
{ get; set; } } 
【注意】
正因为这样,我们一般都会现将结果保存到 MemoryStream 之中,当没有抛出异常之后再将这些数据写入到文件/网络。
Serializable 特性
Serializable 特性只能用于值类型、引用类型、枚举类型(默认)、委托类型(默认),而且是不可被子类继承。
如果有一个 A 类与其派生类 B 类,那么 A 类没拥有 Serializable 特性,而子类拥有,一样的是无法进行序列化操作。
而且序列化的时候,是将所有访问级别的字段成员都进行了序列化,包括 private 级别成员。
四、简单控制序列化操作
禁止序列化某个字段
可以通过 System.NonSerializedAttribute 特性来确保某个字段在序列化时不被处理其值,例如下列代码:
[Serializable] public class DIYClass { public DIYClass() { x = 10; y = 100; z 
= 1000; } public int x { get; set; } public int y { get; set; } [NonSerialized] 
public int z; } 
在序列化之前,该自定义对象 z 字段的值为 1000,在序列化时,检测到了忽略特性,则不会写入该字段的值到流当中。并且在反序列化之后,z 的值为 0,而 x 
,y 的值是 10 和 100。
序列化与反序列化的四个生命周期特性
通过 OnSerializing 、OnSerialized、OnDeserializing、OnDeserialized 
这四个特性,我们可以在对象序列化与反序列化时进行一些自定义的控制。只需要将这四个特性分别加在四个方法上面即可,但是针对方法签名必须返回值为 
void,同时也需要用有一个StreamingContext 参数。
而且一般建议将这四个方法标识为 private ,防止其他对象误调用。
[Serializable] public class DIYClass { [OnDeserializing] private void 
OnDeserializing(StreamingContext context) { 
Console.WriteLine("反序列化的时候,会调用本方法."); } [OnDeserialized] private void 
OnDeserialized(StreamingContext context) { 
Console.WriteLine("反序列化完成的时候,会调用本方法."); } [OnSerializing] public void 
OnSerializing(StreamingContext context) { Console.WriteLine("序列化的时候,会调用本方法."); 
} [OnSerialized] public void OnSerialized(StreamingContext context) { 
Console.WriteLine("序列化完成的时候,会调用本方法."); } } 
【注意】
如果 A 类型有两个版本,第 1 个版本有 5 个字段,并被序列化存储到了文件当中。后面由于业务需要,针对于 A 类型增加了 2 
个新的字段,这个时候如果从文件中读取第 1 个版本的对象流信息,就会抛出异常。
我们可以通过 System.Runtime.Serialization.OptionalFieldAttribute 
添加到我们新加的字段之上,这样的话在反序列化数据时就不会因为缺少字段而抛出异常。
五、格式化器的序列化原理
格式化器的核心就是 FCL 提供的 FormatterServices 的静态工具类,下列步骤体现了序列化器如何结合 FormatterServices 
工具类来进行序列化操作的。
 * 格式化器调用 FormatterService.GetSerializableMembers() 方法获得需要序列化的字段构成的 MemberInfo 
数组。 
 * 格式化器调用 FormatterService.GetObjectData() 方法,通过之前获取的字段 MethodInfo 
信息来取得每个字段存储的值数组。该数组与字段信息数组是并行的,下标一致。 
 * 格式化器写入类型的程序集等信息。 
 * 遍历两个数组,写入字段信息与其数据到流当中。 
反序列化操作的步骤与上面相反。
 * 首先从流头部读取程序集标识与类型信息,如果当前 AppDomain 没有加载该程序集会抛出异常。如果类型的程序集已经加载,则通过 
FormatterServices.GetTypeFromAssembly() 方法来构造一个 Type 对象。 
 * 格式化器调用 FormatterService.GetUninitializedObject() 方法为新对象分配内存,但是 不会调用对象的构造器。 
 * 格式化器通过 FormatterService.GetSerializableMembers() 初始化一个 MemberInfo 数组。 
 * 格式化器根据流中的数据创建一个 Object 数组,该数组就是字段的数据。 
 * 格式化器通过 FormatterService.PopulateObjectMembers() 
方法,传入新分配的对象、字段信息数组、字段数据数组进行对象初始化。 
六、控制序列化/反序列化的数据
一般来说通过在第四节说的那些特性控制就已经满足了大部分需求,但格式化器内部使用的是反射,反射性能开销比较大,如果你想要针对序列化/反序列化进行完全的控制,那么你可以实现
ISerializable 接口来进行控制。
该接口只提供了一个 GetObjectData() 方法,原型如下:
public interface ISerializable{ void GetObjectData(SerializationInfo 
info,StreamingContext context); } 
【注意】
使用了 ISerializable 接口的代价就是其集成类都必须实现它,而且还要保证子类必须调用基类的 GetObjectData() 
方法与其构造函数。一般来说密封类才使用ISerializable ,其他的类型使用特性控制即可满足。
另外为了防止其他的代码调用 GetObjectData() 方法,可以通过一下特性来防止误操作:
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = 
true)] 
如果格式化器检测到了类型实现了该接口,则会忽略掉原有的特性,并且将字段值传入到 SerializationInfo 之中。
通过这个 Info 我们可以被序列化的类型,因为 Info 提供了 FullTypeName 与 AssemblyName,不过一般推荐使用该对象提供的 
SetType(Type type) 方法来进行操作。
格式化器构造完成 Info 之后,则会调用 GetObjectData() 方法,这个时候将之前构造好的 Info 
传入,而该方法则决定需要用哪些数据来序列化对象。这个时候我们就可以通过 Info 的AddValue() 方法来添加一些信息用于反序列化时使用。
在反序列化的时候,需要类型提供一个特殊的构造函数,对于密封类来说,该构造函数推荐为 private ,而一般的类型推荐为 
protected,这个特殊的构造函数方法签名与GetObjectData() 一样。
因为在反序列化的时候,格式化器会调用这个特殊的构造函数。
以下代码就是一个简单实践:
public class DIYClass : ISerializable { public int X { get; set; } public int 
Y { get; set; } public DIYClass() { } protected DIYClass(SerializationInfo 
info, StreamingContext context) { X = info.GetInt32("X"); Y = 20; } public void 
GetObjectData(SerializationInfo info, StreamingContext context) { 
info.AddValue("X", 10); } } 
该类型的对象在反序列化之后,X 的值为序列化之前的值,而 Y 的值始终都会为 20。
【注意】
如果你存储的 X 值是 Int32 ,而在获取的时候是通过 GetInt64() 进行获取。那么格式化器就会尝试使用 System.Convert 
提供的方法进行转换,并且可以通过实现IConvertible 接口来自定义自己的转换。
不过只有在 Get 方法转换失败的情况下才会使用上述机制。
子类与基类的 ISerializable
如果某个子类集成了基类,那么子类在其 GetObjectData() 与特殊构造器中都要调用父类的方法,这样才能够完成正确的序列化/反序列化操作。
如果基类没有实现 ISerializable 接口与特殊的构造器,那么子类就需要通过 FormatterService 来手动针对基类的字段进行赋值。
七、流上下文
流上下文 StreamingContext 只有两个属性,第一个是状态标识位,用于标识序列化/反序列化对象的来源与目的地。而第二个属性就是一个 Object 
引用,该引用则是一个附加的上下文信息,由用户进行提供。
八、类型序列化为不同的类型与对象反序列化为不同的对象
在某些时候可能需要更改序列化完成之后的对象类型,这个时候只需要对象在其实现 ISerializable 接口的 GetObjectData() 方法内部通过 
SerializationInfo 的 SetType() 方法变更了序列化的目标类型。
下面的代码演示了如何序列化一个单例对象:
[Serializable] public sealed class Singleton : ISerializable { private static 
readonly Singleton _instance = new Singleton(); private Singleton() { } public 
static Singleton GetSingleton() { return _instance; } 
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter 
=true)] void ISerializable.GetObjectData(SerializationInfo info, 
StreamingContext context) { info.SetType(typeof(SingletonHelper)); } } 
这里通过显式实现接口的 GetObjectData() 方法来将序列化的目标类型设置为 SingletonHelper ,该类型的定义如下:
[Serializable] public class SingletonHelper : IObjectReference { public object 
GetRealObject(StreamingContext context) { return Singleton.GetSingleton(); } } 
这里因为 SingletonHelper 实现了 IObjectReference 接口,当格式化器尝试进行反序列化的时候,由于在 
GetObjectData() 欺骗了转换器,因此反序列化的时候检测到类型有实现该接口,所以会尝试调用其 GetRealObject() 
方法来进行反序列化操作。
而以上动作完成之后,SingletonHelper 会立即变为不可达对象,等待 GC 进行回收处理。
九、序列化代理
当某些时候需要对一个第三方库对象进行序列化的时候,没有其源码,但是想要进行序列化,则可以通过序列化代理来进行序列化操作。
要实现序列化代理,需要实现 ISerializationSurrogate 接口,该接口拥有两个方法,其签名分别如下:
void GetObjectData(Object obj,SerializationInfo info,StreamingContext 
context); void SetObjectData(Object obj,SerializationInfo info,StreamingContext 
context,ISurrogateSelector selector); 
GetObjectData() 方法会在对象序列化时进行调用,而 SetObjectData() 会在对象反序列化时调用。
比如说我们有一个需求是希望 DateTime 类型在序列化的时候通过 UTC 时间序列化到流中,而在反序列化时则更改为本地时间。
这个时候我们就可以自己实现一个序列化代理类 UTCToLocalTimeSerializationSurrogate:
public sealed class UTCToLocalTimeSerializationSurrogate : 
ISerializationSurrogate { public void GetObjectData(object obj, 
SerializationInfo info, StreamingContext context) { info.AddValue("Date", 
((DateTime)obj).ToUniversalTime().ToString("u")); } public object 
SetObjectData(object obj, SerializationInfo info, StreamingContext context, 
ISurrogateSelector selector) { return 
DateTime.ParseExact(info.GetString("Date"), "u", null).ToLocalTime(); } } 
并且在使用的时候,通过构造一个 SurrogateSelector 代理选择器,传入我们针对于 DateTime 
类型的代理,并且将格式化器与代理选择器相绑定。那么在使用格式化器的时候,就会通过我们的代理类来处理DateTime 类型对象的序列化/反序列化操作了。
static void Main(string[] args) { using (var stream = new MemoryStream()) { 
var formatter = new BinaryFormatter(); // 创建一个代理选择器 var ss = new 
SurrogateSelector(); // 告诉代理选择器,针对于 DateTime 类型采用 UTCToLocal 代理类进行序列化/反序列化代理 
ss.AddSurrogate(typeof(DateTime), formatter.Context, new 
UTCToLocalTimeSerializationSurrogate()); // 绑定代理选择器 formatter.SurrogateSelector 
= ss; formatter.Serialize(stream,DateTime.Now); stream.Position = 0; var 
oldValue = new StreamReader(stream).ReadToEnd(); stream.Position = 0; var 
newValue = (DateTime)formatter.Deserialize(stream); 
Console.WriteLine(oldValue); Console.WriteLine(newValue); } Console.ReadLine(); 
} 
而一个代理选择器允许绑定多个代理类,选择器内部维护一个哈希表,通过 Type 与 StreamingContext 作为其键来进行搜索,通过 
StreamintContext 地不同可以方便地为 DateTime 类型绑定不同用途的代理类。
十、反序列化对象时重写程序集/类型
通过继承 SerializationBinder 抽象类,我们可以很方便地实现类型反序列化时转化为不同的类型,该抽象类有一个 Type 
BindToType(String assemblyName,String typeName) 方法。
重写该方法你就可以在对象反序列化时,通过传入的两个参数来构造自己需要返回的真实类型。第一个参数是程序集名称,第二个参数是格式化器想要反序列化时转换的类型。
编写好 Binder 类重写该方法之后,在格式化器的 Binder 属性当中绑定你的 Binder 类即可。
【注意】
抽象类还有一个 BindToName() 方法,该方法是在序列化时被调用,会传入他想要序列化的类型。
热门工具 换一换