不修改某类型的代码,也能重写该类型的对象的序列化和反序列化?
对于很多第三方类库的代码,如果里面的类型没有定义成可序列化的,但是我们也想对其进行序列化操作,我们通过学习本文就可以做到。
应用程序之所以要重写某类型的序列化和反序列化,主要因为以下三个方面:
1. 允许开发人员序列化最初没有设计成可序列化的类型。
2. 允许开发人员提供一种方式将类型的一个版本映射到另一个的版本。
3. 允许开发人员重写默认的序列化逻辑。
为了达到目的,需要做一下几步:
1. 我们需要一个“代理类型”,它会重写目标类型的序列化和反序列化的逻辑。
2. 向格式化器注册“代理类型”和目标类型对应关系。
3. 格式化器对目标类型的实例进行序列化或反序列化,就会调用“代理类型”里面定义的方法。
序列化代理类型必须实现该接口:
//namespace System.Runtime.Serialization
public interface ISerializationSurrogate
{
void GetObjectData(object obj, SerializationInfo info, StreamingContext context);
object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector);
}
GetObjectData方法和ISerializable接口的GetObjectData方法差不多,只是多了一个参数obj,它就是要序列化的真实对象的引用。
SetObjectData方法用于反序列化一个对象,object obj对象就是要反序列出来的对象,但是它的字段都没有初始化好,可以设置好这个对象的字段,然后SetObjectData方法返回null,序列化器会知道应该使用object obj对象作为反序列出来的对象。也可以无视object obj对象,在SetObjectData方法内部创建一个完全不同的对象,甚至创建不同类型的对象,并返回该对象,那么序列化器会将SetObjectData方法返回的对象作为反序列出来的对象。通过SerializationInfo info对象可以获取流中的数据。
我们写一个例子,把一个本地时间的DateTime对象序列化到流中,并保存为UTC时间。反序列化的时候,从流中取出UTC时间,并转换成本地时间,然后返回。
class UniversalToLocalTimeSerializationSurrogate : ISerializationSurrogate
{
void ISerializationSurrogate.GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
string uDateTimeStr = ((DateTime)obj).ToUniversalTime().ToString("u");
info.AddValue("Date", uDateTimeStr);
}
object ISerializationSurrogate.SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
string uDateTimeStr = info.GetString("Date");
DateTime uDateTime = DateTime.ParseExact(uDateTimeStr, "u", null);
DateTime localDateTime = uDateTime.ToLocalTime();
return localDateTime;
}
}
using (MemoryStream stream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
SurrogateSelector ss = new SurrogateSelector();
ss.AddSurrogate(typeof(DateTime), formatter.Context, new UniversalToLocalTimeSerializationSurrogate());
formatter.SurrogateSelector = ss;
DateTime localDateTime = DateTime.Now;
formatter.Serialize(stream, localDateTime);
stream.Position = 0;
var localDateTime2 = (DateTime)formatter.Deserialize(stream);
}
分析上面的代码,
1. 代理序列化类和目标类是注册在SurrogateSelector对象里面的。
2. SurrogateSelector对象传给格式化器的SurrogateSelector属性。
完成这两步,格式化器就知道了所有的代理序列化类和目标类的对应关系。
当调用格式化器的Serialize方法时,格式化器会在SurrogateSelector属性中查找要序列化的目标类型,如果匹配到一个代理类,就会调用代理类的GetObjectData方法,来负责写入流的信息。
当调用格式化器的Deserialize方法时,格式化器会在SurrogateSelector属性中查找要反序列化的目标类型,如果匹配到一个代理类,就会调用代理类的SetObjectData方法,来负责要反序列化对象的设置。
代理选择器链
FCL默认实现的SurrogateSelector类其实实现了ISurrogateSelector接口。这个接口定义如下:
public interface ISurrogateSelector
{
void ChainSelector(ISurrogateSelector selector);
ISurrogateSelector GetNextSelector();
ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, out ISurrogateSelector selector);
}
通过调用ChainSelector方法,可以把多个ISurrogateSelector对象链接在一起形成一个链。根据业务场景不同,我们可以分别使用不同的ISurrogateSelector对象来维护代理序列化类和目标类的映射关系。但是最后一定要把不同的ISurrogateSelector对象链接起来。GetSurrogate方法会在当前的ISurrogateSelector对象中查找目标类,如果找不到就访问链中的下一个ISurrogateSelector对象,直到找到目标类,并返回匹配的代理类。如果链中的所有ISurrogateSelector对象都找不到要序列化的目标类,就会返回null。
回顾全文:
-纸上得来终觉浅,绝知此事要躬行-