《CLR Via C#》读书笔记:24.运行时序列化
一、什么是运行时序列化
序列化的作用就是将对象图(特定时间点的对象连接图)转换为字节流,这样这些对象图就可以在文件系统/网络进行传输。
二、序列化/反序列化快速入门
一般来说我们通过 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()
方法,该方法是在序列化时被调用,会传入他想要序列化的类型。
上一篇: 冬至进补有针对 哪些人最需调养
下一篇: 老人度夏要养“气阴”