专栏首页明丰随笔运行时序列化 2

运行时序列化 2

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

OnSerializing

OnSerialized

OnDeserializing

OnDeserialized

NonSerialized

OptionalField

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

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方法,通过它可以把对象序列化到数据流。实现这个接口的同时还应该实现一个特殊的构造器,通过这个特殊的构造器可以把数据流反序列化到对象。这个特殊构造器签名:

private ClassXXX(SerializationInfo info, StreamingContext context)
{
  ...
}

快速入门:

[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接口和特殊构造器添加如下特性,以防止其他代码调用它们:

[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字段,就没法实现。

示例代码:

[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接口实现和基类之间的关系

本文分享自微信公众号 - 明丰随笔(liumingfengwx2),作者:刘明丰

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 运行时序列化 4

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

    小蜜蜂
  • 运行时序列化 1

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

    小蜜蜂
  • WCF学习笔记 4

    其中应用在ServiceContractAttribute服务契约特性上的特性[AttributeUsage(AttributeTargets.Class)]表...

    小蜜蜂
  • Java基础题

    不能。在Java中,只要是被定义为final的类,也可以说是被final修饰的类,就是不能被继承的。

    用户3467126
  • 存储03-存储与主机接口的变迁史

    2)要想提高单位时间内的通行量,有两种方案:增多车道(并行通道增多)和提高车速(提高传输频率)

    大话IT架构
  • 区块链技术公司 聊区块链+积分体系应用

    2018年的商业热点几乎被区块链技术公司覆盖,而“积分体系”则是区块链技术开发的潜力领域!在应用空间上,积分被应用在超市、银行、酒店、餐厅等各种商户类型中!在吸...

    区块链开发先驱者
  • NLP将迎来黄金十年,7个案例带你入门(附Python代码)

    导读:近日,微软研究院发文称,NLP即将迎来“黄金十年”。他们认为,各领域对NLP的需求会大幅度上升,对NLP质量也提出更高要求。如果你想赶上这“黄金十年”,现...

    华章科技
  • 算法提高 01背包

    问题描述   给定N个物品,每个物品有一个重量W和一个价值V.你有一个能装M重量的背包.问怎么装使得所装价值最大.每个物品只有一个. 输入格式   ...

    AI那点小事
  • Idea 突然打不开了

    1、下载压缩包解压后得到jetbrains-agent.jar 2、启动IDEA,试用(Evaluate for free)进入IDE 3、

    OPice
  • Java虚拟机的Heap监狱

    在Java虚拟机中,我是一个位高权重的大管家,他们都很怕我,尤其是那些Java 对象,我把他们圈到一个叫做Heap的“监狱”里,严格管理,生杀大权尽在掌握。

    用户1260737

扫码关注云+社区

领取腾讯云代金券