一、什么是运行时序列化

序列化的作用就是将对象图(特定时间点的对象连接图)转换为字节流,这样这些对象图就可以在文件系统/网络进行传输。

二、序列化/反序列化快速入门

一般来说我们通过 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() 方法,该方法是在序列化时被调用,会传入他想要序列化的类型。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信