首页
学习
活动
专区
圈层
工具
发布

运行时序列化 4

不修改某类型的代码,也能重写该类型的对象的序列化和反序列化?

对于很多第三方类库的代码,如果里面的类型没有定义成可序列化的,但是我们也想对其进行序列化操作,我们通过学习本文就可以做到。

应用程序之所以要重写某类型的序列化和反序列化,主要因为以下三个方面:

1. 允许开发人员序列化最初没有设计成可序列化的类型。

2. 允许开发人员提供一种方式将类型的一个版本映射到另一个的版本。

3. 允许开发人员重写默认的序列化逻辑。

为了达到目的,需要做一下几步:

1. 我们需要一个“代理类型”,它会重写目标类型的序列化和反序列化的逻辑。

2. 向格式化器注册“代理类型”和目标类型对应关系。

3. 格式化器对目标类型的实例进行序列化或反序列化,就会调用“代理类型”里面定义的方法。

序列化代理类型必须实现该接口:

代码语言:javascript
复制
//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时间,并转换成本地时间,然后返回。

代码语言:javascript
复制
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接口。这个接口定义如下:

代码语言:javascript
复制
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。

回顾全文:

  1. “序列化代理类型”和目标类型一一对应
  2. 对应关系维护在ISurrogateSelector对象中
  3. 多个ISurrogateSelector对象形成链
  4. 设置格式化器的SurrogateSelector属性
  5. 格式化器的序列化和反序列化会查找SurrogateSelector里面的注册信息,根据查找结果最终决定是否使用序列化代理类来执行序列化的动作

-纸上得来终觉浅,绝知此事要躬行-

下一篇
举报
领券