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

运行时序列化 2

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

上一篇文章讨论过,控制序列化和反序列化过程的最佳方式就是使用以下特性:

OnSerializing

OnSerialized

OnDeserializing

OnDeserialized

NonSerialized

OptionalField

使用这些特性是基于反射技术完成的,性能有一点损伤。还有在极少数的情况下,这些特性不能提供你想要的全部控制。你的类型通过实现ISerializable接口,也能支持运行时序列化,该接口定义如下:

代码语言:javascript
复制
public interface ISerializable
{
  /// <summary>Populates a <see cref="T:System.Runtime.Serialization.SerializationInfo" /> with the data needed to serialize the target object.</summary>
  /// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> to populate with data. </param>
  /// <param name="context">The destination (see <see cref="T:System.Runtime.Serialization.StreamingContext" />) for this serialization. </param>
  /// <exception cref="T:System.Security.SecurityException">The caller does not have the required permission. </exception>
  [SecurityCritical]
  void GetObjectData(SerializationInfo info, StreamingContext context);
}

这个接口只有一个GetObjectData方法,通过它可以把对象序列化到数据流。实现这个接口的同时还应该实现一个特殊的构造器,通过这个特殊的构造器可以把数据流反序列化到对象。这个特殊构造器签名:

代码语言:javascript
复制
private ClassXXX(SerializationInfo info, StreamingContext context)
{
  ...
}

快速入门:

代码语言:javascript
复制
[Serializable]
public sealed class Product : ISerializable
{
  private string _name;
  public string Name
  {
    get { return this._name; }
    set { this._name = value; }
  }
  public Product() { }
  [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
  private Product(SerializationInfo info, StreamingContext context)
  {
    //_name = (String)info.GetValue("Name", typeof(string));
    _name = (String)info.GetString("Name");
  }
  [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
  void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
  {
    //info.AddValue("Name", _name, typeof(string));
    info.AddValue("Name", _name);
  }
}

var formatter = new BinaryFormatter();
Stream stream = new MemoryStream();
var product = new Product { Name = "Hello" };

formatter.Serialize(stream, product);

stream.Position = 0;
var product2 = (Product)formatter.Deserialize(stream);

重要提示:

1. 某类型一旦实现ISerializable接口,它的所有的派生类型也必须实现它,而且派生类必须保证调用基类的GetObjectData方法和特殊构造器。

2. 某类型一旦实现ISerializable接口,便不能删除它,否则会失去与派生类型的兼容性。所以密封类实现ISerializable接口是最让人放心的。

3. ISerializable接口和特殊构造器是给格式化器调用,其他代码不应该调用它们。类型在实现ISerializable接口的时候,应该使用显式实现。

4. 某类型在定义特殊构造器的时候,如果类型是密封类,那么它的特殊构造器应该声明成private的。否则应该声明成protected的,将来需要给它的子类的特性构造器调用。

5. 为ISerializable接口和特殊构造器添加如下特性,以防止其他代码调用它们:

代码语言:javascript
复制
[SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]

格式化器序列化对象图时,会检查每一个对象。如果发现对象的类型实现了ISerializable接口,就会忽略所有的定制特性,改为构造新的SerializationInfo对象。该对象包含了需要序列化的对象的字段的集合。

构造SerializationInfo对象时,格式化器要提供两个参数:Type和IFormatterConverter。

Type参数代表了要序列化的对象的类型。通过Type参数,可以得到类型的全名(Type.FullName)和程序集标识(Type.Module.Assembly.FullName)。

可以访问SerializationInfo对象的FullTypeName属性和AssemblyName属性得到它们。如果想要修改这两个属性,可以调用SerializationInfo对象的SetType方法完成。

构造好并初始化好SerializationInfo对象之后,格式化器调用类型的GetObjectData方法,向它传递SerializationInfo对象的引用,GetObjectData方法决定需要哪些信息来序列化对象,并将这些信息添加到SerializationInfo对象中,SerializationInfo类型提供了众多重载AddValue方法,我们想序列化哪个字段就对这个字段调用一次AddValue方法。GetObjectData方法添加好所有必要的序列化信息之后,会返回格式化器。现在,格式化器获取已添加到SerializationInfo对象的所有值,并把它们都序列化到流中。

完成了序列化工作之后,再来看反序列化,格式化器从流中提取一个对象时,会为新对象分配内存,通过调用FormatterServices.GetUninitializedObject方法,然后格式化器会检查类型是否实现了ISerializable接口,如果存在这个接口,格式化器就尝试调用那个特殊构造器。它的参数和GetObjectData方法完全一致。构造器获取SerializationInfo对象引用,在这个对象中,包含了对象序列化时添加的所有值,特殊构造器可调用GetXXX等任何一个方法(XXX是具体的数据类型比如Char,Int16,Int32等等),向它传递与序列化同一字段对应的字符串,可以返回流中的字段的值,并用返回值初始化新对象的各个字段。

反序列化对象的字段时,应调用和对象序列化时传给AddValue方法的值的类型匹配的Get方法。比如在GetObjectData方法调用AddValue方法传递的是Int32值,那么在反序列化对象时,应该为同一值调用GetInt32方法。如果值在流中的类型和你试图获取的Get方法类型不符,格式化器会尝试调用IFormatterConverter接口将流中的值转型为你制定的类型。格式化器会负责构造SerializationInfo对象,但是格式化器没有提供任何方式让你选择不同的IFormatterConverter类型,FCL提供了IFormatterConverter接口的默认实现FormatterConverter类。在FormatterConverter类中,调用了Convert类的各种静态方法在不同的核心类型之间对值进行转换,比如将Int32转换成一个Int64. 然而,为了能在其他任意类型之间转换,FormatterConverter类要调用Convert的ChangeType方法,将序列化好的类型转型为一个IConvertible接口,再调用恰当的接口方法。所以要运行一个可序列化类型的对象反序列化成一个不同的类型,可以考虑让自己的类型实现IConvertible接口。

特殊构造器也可以不调用上面的GetXXX方法,而是调用GetEnumerator。该方法返回SerializationInfoEnumerator对象,可用该对象遍历SerializationInfo对象中包含的所有值。枚举的每一个值都是一个SerializationEntry对象。

对于已经实现了ISerializable接口和特殊构造器的类型,当我们定义它的派生类的时候,必须保证调用基类的GetObjectData方法和特殊构造器。否则对象是不能正确序列化和反序列化的。如果你的派生类中没有任何额外的字段,因而没有特殊的序列化/反序列化需求,就完全不必实现ISerializable。接口的GetObjectData方法是virtual的,特殊构造器也是virtualized的,在反序列期间,格式化器会检查要实例化的类型,如果那个类型没有提供特殊构造器,格式化器会扫描基类,直到找到实现了特殊构造器的类。

重要提示:

特殊构造器中的代码一般从传递给它的SerializationInfo对象中提取字段,提取好字段之后,并不能保证对象已经完全反序列化好了,尤其当对象内部引用另一个类型的字段。如果这个时候你想调用对象的成员,建议你的类型提供一个应用了OnDeserialized特性的方法,或者让类型实现IDeserializationCallback接口的OnDeserialization方法。构造器在调用这两个方法的时候,对象的所有字段应该都已经设置好了。对于对象图来说,它们的OnDeserialized方法的调用顺序是没有保障的,所以虽然字段可能已初始化好,但是仍然不能确定被引用的对象是否已完全发序列化好。

要实现ISerializable但基类没有实现怎么办?

ISerializable接口功能非常强大,运行类型可以完全控制如何对类型的实例进行序列化和反发序列化。现在,当派生类实现ISerializable接口的时候,还要负责它的基类的字段的序列化,如果基类已经实现了ISerializable接口,那么对基类的字段进行序列化就非常简单,调用基类的GetObjectData方法。但是当基类没有实现ISerializable接口的时候,派生类必须手动序列化基类的字段,具体的做法是获取它们的值,并把这些值添加到SerializationInfo对象里。当反序列化的时候,在特殊构造器中,还必须从SerializationInfo对象中取出这些值,并以某种方式设置基类的字段。如果基类的字段是public或protected的,那么一切都很容易实现。如果是private字段,就没法实现。

示例代码:

代码语言:javascript
复制
[Serializable]
class MyBase
{
  protected string _name = "Nestor";
  public MyBase() { }
}

[Serializable]
class MyDerived : MyBase, ISerializable
{
  private DateTime _dateTime = DateTime.Now;
  public MyDerived() { }
  //如果这个构造器不存在,便会引发一个SerializationException异常
  //如果这个类不是密封类,这个构造器就应该是protected的
  [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
  private MyDerived(SerializationInfo info, StreamingContext context)
  {
    Type baseType = this.GetType().BaseType;
    MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType, context);
    for (int i = 0; i < memberInfos.Length; i++)
    {
      FieldInfo fieldInfo = (FieldInfo)memberInfos[i];
      string serializedName = baseType.FullName + "+" + fieldInfo.Name;
      var fieldVal = info.GetValue(serializedName, fieldInfo.FieldType);
      fieldInfo.SetValue(this, fieldVal);
    }
    _dateTime = info.GetDateTime("Date");
  }
  [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
  void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
  {
    info.AddValue("Date", _dateTime);
    Type baseType = this.GetType().BaseType;
    MemberInfo[] memberInfos = FormatterServices.GetSerializableMembers(baseType, context);
    for (int i = 0; i < memberInfos.Length; i++)
    {
      FieldInfo fieldInfo = (FieldInfo)memberInfos[i];
      string serializedName = baseType.FullName + "+" + fieldInfo.Name;
      var fieldVal = fieldInfo.GetValue(this);
      info.AddValue(serializedName, fieldVal);
    }
  }
  public override string ToString()
  {
    return string.Format("Name={0}, Date={1}", _name, _dateTime);
  }
}

var formatter = new BinaryFormatter();
Stream stream = new MemoryStream();
MyDerived myDerived = new MyDerived();
formatter.Serialize(stream, myDerived);

stream.Position = 0;
var myDerived2 = (MyDerived)formatter.Deserialize(stream);

文章回顾:

实现ISerializable接口,也能支持运行时序列化

特殊的构造器可以把数据流反序列化到对象

ISerializable接口实现和基类之间的关系

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

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

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

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

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