前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >运行时序列化 1

运行时序列化 1

作者头像
小蜜蜂
发布2019-07-17 15:14:57
4450
发布2019-07-17 15:14:57
举报
文章被收录于专栏:明丰随笔明丰随笔

序列化是将对象或对象图转成字节流的过程。

反序列化是将字节流转换回对象或对象图的过程。

序列化与反序列化的意义

1. 把对象图的状态保存到文件或数据库中,在下次需要的时候可以还原。

2. 把对象图的转成字节流之后,可以通过网络传输到远程机器,远程机器可以还原对象图信息。

3. 利用这个技术可以跨越AppDomain边界发送对象。

4. 对象图转成字节流之后,可以方便进行加密和压缩。

.Net Framework内建了出色的序列化和反序列化支持。对于几乎所有的类型,这些默认的行为已经足够了。如果默认行为不能满足,序列化的扩展性极佳,我们可以自定义序列化里面的细节。

快速入门:

代码语言:javascript
复制
private static void QuickStart()
{
  var objectGraph = new List<string> { "Nestor", "Liu" };
  Stream stream = SerializeToMemory(objectGraph);
  stream.Position = 0;
  objectGraph = null;
  objectGraph = DeserializeFromMemory(stream);
  foreach (var s in objectGraph)
  {
    Console.WriteLine(s);
  }
}
private static Stream SerializeToMemory(List<string> objectGraph, Stream stream = null)
{
  stream = stream ?? new MemoryStream();
  BinaryFormatter formatter = new BinaryFormatter();
  formatter.Serialize(stream, objectGraph);
  return stream;
}
private static List<string> DeserializeFromMemory(Stream stream)
{
  BinaryFormatter formatter = new BinaryFormatter();
  return (List<string>)formatter.Deserialize(stream);
}

小技巧:利用序列化实现深度克隆

代码语言:javascript
复制
private static object DeepClone(object original)
{
  using (MemoryStream stream = new MemoryStream())
  {
    BinaryFormatter formatter = new BinaryFormatter();
    formatter.Context = new StreamingContext(StreamingContextStates.Clone);
    formatter.Serialize(stream, original);
    stream.Position = 0;
    return formatter.Deserialize(stream);
  }
}

在序列化中主要有三个部分:对象图,字节流,序列化格式器

CLR的运行时序列化技术对CLR数据类型有很深刻的理解,能将对象的所有public,protected,private字段序列化成二进制流中。

对于运行时序列化格式器,它需要是实现IFormatter接口类型

代码语言:javascript
复制
public interface IFormatter
{
  ...
  object Deserialize(Stream serializationStream);
  void Serialize(Stream serializationStream, object graph);
  ...
}

FCL提供了两个格式化器,BinaryFormatter和SoapFormatter。命名空间是:

代码语言:javascript
复制
System.Runtime.Serialization.Formatters.Binary
System.Runtime.Serialization.Formatters.Soap

我们看一下Binary的Serialize方法实现:

代码语言:javascript
复制
void Serialize(Stream serializationStream, object graph, Header[] headers, bool fCheck)
{
  InternalFE internalFE = new InternalFE()
  {
    FEtypeFormat = this.m_typeFormat,
    FEserializerTypeEnum = InternalSerializerTypeE.Binary,
    FEassemblyFormat = this.m_assemblyFormat
  };
  ObjectWriter objectWriter = new ObjectWriter(this.m_surrogates, this.m_context, internalFE, this.m_binder);
  __BinaryWriter _BinaryWriter = new __BinaryWriter(serializationStream, objectWriter, this.m_typeFormat);
  objectWriter.Serialize(graph, headers, _BinaryWriter, fCheck);
  this.m_crossAppDomainArray = objectWriter.crossAppDomainArray;
}

我们看一下Binary的Deserialize方法实现:

代码语言:javascript
复制
object Deserialize(Stream serializationStream, HeaderHandler handler, bool fCheck, bool isCrossAppDomain, IMethodCallMessage methodCallMessage)
{
  InternalFE internalFE = new InternalFE()
  {
    FEtypeFormat = this.m_typeFormat,
    FEserializerTypeEnum = InternalSerializerTypeE.Binary,
    FEassemblyFormat = this.m_assemblyFormat,
    FEsecurityLevel = this.m_securityLevel
  };
  ObjectReader objectReader = new ObjectReader(serializationStream, this.m_surrogates, this.m_context, internalFE, this.m_binder)
  {
    crossAppDomainArray = this.m_crossAppDomainArray
  };
  return objectReader.Deserialize(handler, new __BinaryParser(serializationStream, objectReader), fCheck, isCrossAppDomain, methodCallMessage);
}

格式化器参考每个对象的类型的元数据,从而了解如何序列化完整的对象图。

Serialize方法利用反射来查看对象有哪些实例字段,这些字段任何引用了其他对象,格式化器就知道那些对象也要进行序列化。

Deserialize方法检查流的内容,根据流构造出对象图,并初始化它们的字段信息,使它们具有与当初序列化时相同的值。通常要返回应用程序期待的类型的对象。

注意事项:

1.序列化和反序列化应该使用相同的格式化器

2.可将多个对象图多次序列化到同一个流中。记住它们的顺序。

3.序列化对象时,类型的全名和程序集全名会被写入流中;反序列化对象时,格式化器首先获取程序集标识信息,并通过Assembly.Load加载程序集。加载之后,格式化器在程序集中查找与要反序列化的对象匹配的类型。找到就创建类型的实例,并用流中包含的值对其字段进行初始化。如果类型中的字段与流中读取的字段名不完全匹配,就拋SerializationException异常。

4.对需要序列化的类,我们定义的时候,不要使用“自动实现的属性”功能来定义属性,因为自动实现的属性在每次编译的时候会生成不同名称的字段,这会影响序列化功能。

将多个对象序列化到同一流中:

代码语言:javascript
复制
private static void MutipleObjsSerializeToStream()
{
  var objectGraph1 = new List<string> { "Nestor", "Liu" };
  var objectGraph2 = new List<string> { "Nestor2", "Liu2" };
  var stream = SerializeToMemory(objectGraph1);
  stream = SerializeToMemory(objectGraph2, stream);
  stream.Position = 0;
  List<string> objectGraph3 = DeserializeFromMemory(stream);
  List<string> objectGraph4 = DeserializeFromMemory(stream);
}

设计类型时,设计人员必须明确定义类型的实例是否可以序列化,默认是不可以序列化。

我们使用SerializableAttribute特性对类型进行标注,表明这个类型是可以序列化的。SerializableAttribute特性只能使用在引用类型、值类型、枚举类型和委托类型上。其中枚举类型和委托类型总是可以序列化的,所以不必显示声明这个特性。另外,父类声明的Serializable特性不会被子类继承,子类如果需要被序列化也必须加上Serializable特性。如果父类不可序列化,它的子类肯定也不能序列化,因为父类是子类的一部分。

序列化格式化器在执行Serialize方法时,会以为对象是可以序列化的,以及对象内部字段引用的类型也是可以序列化的。然而如果它们是不可以序列化的,就拋SerializationException异常。

因为序列化过程中可能会抛出异常并终止程序,所以我们可以先将对象图序列化到MemoryStream中,保证序列化能够顺利完成,然后再将字节复制到希望的目标流中(文件流和网络流)

将Serializable特性应用于类型,所有实例字段都会被序列化。但是类型可能定义了一些不需要实例化的字段,我们使用NonSerializedAttribute特性应用于不需要序列化的字段。该属性只能应用于字段,并且会被子类继承。

当反序列出来的对象因为部分字段应用了NonSerialized特性,没有被正确设置初始化值,我们可以使用OnDeserialized特性定义OnDeserialized方法,在这个方法中,我们可以根据已有信息对NonSerialized字段进行正确赋值。

和OnDeserialized特性类似的还有:OnSerializingAttribute,OnSerializedAttribute,OnDeserializingAttribute和OnDeserializedAttribute等特性。可将它们应用于类型中定义的方法,对序列化和反序列过程进行更多的控制。

使用这4个属性中的任何一个时,你定义的方法必须传入一个StreamingContext实例参数,并返回void。方法名可以是任意名称。应该将方法声明为private,避免被普通代码调用;运行时格式化器有足够的权限去调用这些方法。

序列化顺序:

1. 调用OnSerializing方法

2. 序列化对象的所有字段

3. 调用OnSerialized方法

反序列化顺序:

1. 调用OnDeserializing方法

2. 反序列化对象的所有字段

3. 调用OnDeserialized方法

在反序列化期间,反序列化一个对象图,对象图中的所有OnDeserialized方法的执行按照倒序执行,因为这样可以使内层对象优先于外层对象完成反序列化,从而是整个对象都会被正确设置。

如果序列化类型的实例,在类型中添加了新字段,然后试图反序列不包含新字段的流数据,格式化器会抛出异常。这不利于版本控制,幸运的是,我们可以使用OptionalFieldAttribute特性声明新字段。这样就不会因为流中数据不包含这个字段而抛出异常了。

格式化器如何序列化类型实例?

在FCL中,提供了System.Runtime.Serialization.FormatterServices类型,是一个静态类型。下面是步骤描述了格式化器是如何完成序列化:

1. 锁定需要实例化的字段。格式化器调用FormatterServices的GetSerializableMembers方法:

代码语言:javascript
复制
private static MemberInfo[] GetSerializableMembers(RuntimeType type)
{
  FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
  int num = 0;
  for (int i = 0; i < (int)fields.Length; i++)
  {
    if ((fields[i].Attributes & FieldAttributes.NotSerialized) != FieldAttributes.NotSerialized)
    {
      num++;
    }
  }
  if (num == (int)fields.Length)
  {
    return fields;
  }
  FieldInfo[] fieldInfoArray = new FieldInfo[num];
  num = 0;
  for (int j = 0; j < (int)fields.Length; j++)
  {
    if ((fields[j].Attributes & FieldAttributes.NotSerialized) != FieldAttributes.NotSerialized)
    {
      fieldInfoArray[num] = fields[j];
      num++;
    }
  }
  return fieldInfoArray;
}

这个方法利用反射技术获取类型的实例字段,返回MemberInfo对象构成的数组,其中每个元素都对应一个可序列化的实例字段。

2. 获取需要实例化的字段值。把MemberInfo[]数组对象,传给FormatterServices的GetObjectData方法,返回object[]数组对象,object[]和MemberInfo[]是一一对应的,object是MemberInfo成员的值。

代码语言:javascript
复制
public static object[] GetObjectData(object obj, MemberInfo[] members)
{
  int length = (int)members.Length;
  object[] objArray = new object[length];
  for (int i = 0; i < length; i++)
  {
    MemberInfo memberInfo = members[i];
    RtFieldInfo rtFieldInfo = memberInfo as RtFieldInfo;
    if (rtFieldInfo == null)
    {
      objArray[i] = ((SerializationFieldInfo)memberInfo).InternalGetValue(obj);
    }
    else
    {
      objArray[i] = rtFieldInfo.UnsafeGetValue(obj);
    }
  }
  return objArray;
}

3. 格式化器将程序集和类型名称写入流中。

4. 格式化器根据MemberInfo[]和object[]的元素,将每一个字段名称和字段值写入流。

下面是步骤描述了格式化器是如何完成反序列化:

1. 格式化器从流中读取程序集和类型名称,并加载程序集,然后调用FormatterServices的GetTypeFromAssembly方法,返回一个Type对象,它代表要反序列化的那个对象的类型。

代码语言:javascript
复制
public static Type GetTypeFromAssembly(Assembly assem, string name)
{
  return assem.GetType(name, false, false);
}

2. 格式化器调用FormatterServices的GetUninitializedObject方法,传入Type对象,返回一个新对象分配的内存,但不调用构造函数,对象的字段被初始化为null或0.

代码语言:javascript
复制
public static object GetUninitializedObject(Type type)
{
  return FormatterServices.nativeGetUninitializedObject((RuntimeType)type);
}

3. 锁定需要实例化的字段。格式化器调用FormatterServices的GetSerializableMembers方法,返回MemberInfo[]数组对象。

4. 格式化器根据流中包含的数据创建并初始化一个object[]数组对象。

5. 将新对象,MemberInfo[]数组对象和object[]数组对象传给FormatterServices的PopulateObjectMembers方法,这个方法遍历数组,将每个字段初始化成对应的值。

代码语言:javascript
复制
public static object PopulateObjectMembers(object obj, MemberInfo[] members, object[] data)
{
  for (int i = 0; i < (int)members.Length; i++)
  {
    MemberInfo memberInfo = members[i];
    if (data[i] != null)
    {
      FormatterServices.SerializationSetValue(memberInfo, obj, data[i]);
    }
  }
  return obj;
}

6. 新对象就被彻底反序列化好了。

文章总结:

序列化概念和意义

序列化中主要有三个部分

格式化器注意事项

NonSerialized和OnSerializedAttribute等特性控制序列化行为

格式化器完成序列化的步骤

下面一片文章将介绍实现ISerializable接口的方式控制序列化。

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 明丰随笔 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档