【问题标题】:How to handle deserialization backward compatibility for changed classes based on generic collections?如何处理基于泛型集合的更改类的反序列化向后兼容性?
【发布时间】:2012-01-25 08:41:20
【问题描述】:

如果旧版本的 c++/CLI 应用序列化了一个类Foo,该类派生自包含X 类型键的字典,而新版本需要将键类型改为Z,那么如何我最好让应用程序支持读取旧的序列化数据(仍然基于X)以及新的序列化数据(基于Z)?

如果以前的情况是这样的:

ref class Foo: Generic::Dictionary<X^, Y^>, ISerializable
{
 public:
  Foo(SerializationInfo^ info, StreamingContext context)
  {
     info->AddValue("VERSION", 1);
     __super::GetObjectData(info, context);
  }

  virtual void GetObjectData(SerializationInfo^ info, StreamingContext context)
     : Generic::Dictionary<X^, Y^>(info, context)
  {
     int version = info->GetInt32("VERSION");
     /* omitted code to check version, act appropriately */
  } 
} 

那么在新的情况下我想做这样的事情:

ref class Foo: Generic::Dictionary<Z^, Y^>, ISerializable
{
 public:
  Foo(SerializationInfo^ info, StreamingContext context)
  {
    info->AddValue("VERSION", 2);
    __super::GetObjectData(info, context);
  }

  virtual void GetObjectData(SerializationInfo^ info, StreamingContext context)
  {
     int version = info->GetInt32("VERSION");
     if (version == 1)
     {
        Generic::Dictionary<X^, Y^> old 
          = gcnew Generic::Dictionary<X^, Y^>(info, context);
        /* code here to convert "old" to new format,
           assign to members of "this" */
     }
     else
     {
        Generic::Dictionary<Z^, Y^)(info, context);
     }
  }
}

但是由于类型的编译错误而失败:

error C2248: 'System::Collections::Generic::Dictionary&lt;TKey,TValue&gt;::Dictionary' : cannot access protected member declared in class 'System::Collections::Generic::Dictionary&lt;TKey,TValue&gt;' with [ TKey=X ^, TValue=Y ^ ].

在更简单的情况下,我可以使用 info-&gt;GetValue 来提取和处理单个数据成员,但在当前情况下,字典的序列化留给 .NET(通过 __super::GetObjectData 调用),我不知道如何使用info-&gt;GetValue提取旧字典。

一个相关问题:如果我想将Foo 重命名为BetterFoo 并且能够支持读取旧的序列化数据(仍然基于Foo)以及新的序列化数据(基于BetterFoo) ,那么我怎样才能最好地做到这一点呢?

我研究了SerializationBinderISerializationSurrogate,但不知道如何使用它们来解决我的问题。

【问题讨论】:

    标签: .net c++-cli deserialization backwards-compatibility


    【解决方案1】:

    我找到了自己问题的部分答案。在调试器中检查SerializationInfoMemberNamesMemberValues 属性会显示其中存储的成员的类型。 Dictionary&lt;X^, Y^&gt; 包含在 SerializationInfo 中,作为名称为 KeyValuePairs 并键入 array&lt;System::Collections::Generic::KeyValuePair&lt;X^, Y^&gt;&gt; ^ 的项目。有了这些信息,SerializationInfoGetValue 方法可以用来检索键值对,然后可以将它们转换并添加到正在填充的对象中。 SerializationBinder 可用于让一个类的反序列化构造函数同时处理另一个类的反序列化,从而在重命名类后允许向后兼容。以下代码显示了所有这些内容。

    using namespace System;
    using namespace System::IO;
    using namespace System::Collections::Generic;
    using namespace System::Runtime::Serialization;
    
    typedef KeyValuePair<int, int> Foo1kvp;
    [Serializable]
    public ref class Foo1: Dictionary<int, int>, ISerializable
    {
    public:
        Foo1() { }
        virtual void GetObjectData(SerializationInfo^ info, StreamingContext context) override
        {
            info->AddValue("VERSION", 1);
            __super::GetObjectData(info, context);
        }
        Foo1(SerializationInfo^ info, StreamingContext context)
        {
            array<Foo1kvp>^ members = (array<Foo1kvp>^) info->GetValue("KeyValuePairs", array<Foo1kvp>::typeid);
            for each (Foo1kvp kvp in members)
            {
                this->Add(kvp.Key, kvp.Value);
            }
            Console::WriteLine("Deserializing Foo1");
        }
    };
    
    typedef KeyValuePair<String^, int> Foo2kvp;
    [Serializable]
    public ref class Foo2: Dictionary<String^, int>, ISerializable
    {
    public:
        Foo2() { }
        virtual void GetObjectData(SerializationInfo^ info, StreamingContext context) override
        {
            info->AddValue("VERSION", 2);
            __super::GetObjectData(info, context);
        }
        Foo2(SerializationInfo^ info, StreamingContext context)
        {
            int version = info->GetInt32("VERSION");
            if (version == 1)
            {
                array<Foo1kvp>^ members = (array<Foo1kvp>^) info->GetValue("KeyValuePairs", array<Foo1kvp>::typeid);
                for each (Foo1kvp kvp in members)
                {
                    this->Add(kvp.Key.ToString(), kvp.Value);
                }
                Console::WriteLine("Deserializing Foo2 from Foo1");
            }
            else
            {
                array<Foo2kvp>^ members = (array<Foo2kvp>^) info->GetValue("KeyValuePairs", array<Foo2kvp>::typeid);
                for each (Foo2kvp kvp in members)
                {
                    this->Add(kvp.Key, kvp.Value);
                }
                Console::WriteLine("Deserializing Foo2");
            }
        }
    };
    
    ref class MyBinder sealed: public SerializationBinder
    {
    public:
        virtual Type^ BindToType(String^ assemblyName, String^ typeName) override
        {
            if (typeName == "Foo1")
                typeName = "Foo2";
            return Type::GetType(String::Format("{0}, {1}", typeName, assemblyName));
        }
    };
    
    int main(array<System::String ^> ^args)
    {
        Console::WriteLine(L"Hello World");
        Foo1^ foo1 = gcnew Foo1;
        foo1->Add(2, 7);
        foo1->Add(3, 5);
    
        IFormatter^ formatter1 = gcnew Formatters::Binary::BinaryFormatter(); // no translation to Foo2
        IFormatter^ formatter2 = gcnew Formatters::Binary::BinaryFormatter();
        formatter2->Binder = gcnew MyBinder; // translate Foo1 to Foo2
        FileStream^ stream;
        try
        {
            // serialize Foo1
            stream = gcnew FileStream("fooserialized.dat", FileMode::Create, FileAccess::Write);
            formatter1->Serialize(stream, foo1);
            stream->Close();
    
            // deserialize Foo1 to Foo1
            stream = gcnew FileStream("fooserialized.dat", FileMode::Open, FileAccess::Read);
            Foo1^ foo1b = dynamic_cast<Foo1^>(formatter1->Deserialize(stream));
            stream->Close();
            Console::WriteLine("deserialized Foo1 from Foo1");
            for each (Foo1kvp kvp in foo1b)
            {
                Console::WriteLine("{0} -> {1}", kvp.Key, kvp.Value);
            }
    
            // deserialize Foo1 to Foo2
            stream = gcnew FileStream("fooserialized.dat", FileMode::Open, FileAccess::Read);
            Foo2^ foo2 = dynamic_cast<Foo2^>(formatter2->Deserialize(stream));
            stream->Close();
            Console::WriteLine("deserialized Foo2 from Foo1");
            for each (Foo2kvp kvp in foo2)
            {
                Console::WriteLine("{0} -> {1}", kvp.Key, kvp.Value);
            }
    
            // serialize Foo2
            Foo2^ foo2b = gcnew Foo2;
            foo2b->Add("Two", 7);
            foo2b->Add("Three", 5);
            stream = gcnew FileStream("fooserialized.dat", FileMode::Create, FileAccess::Write);
            formatter2->Serialize(stream, foo2b);
            stream->Close();
    
            // deserialize Foo2 to Foo2
            stream = gcnew FileStream("fooserialized.dat", FileMode::Open, FileAccess::Read);
            Foo2^ foo2c = dynamic_cast<Foo2^>(formatter2->Deserialize(stream));
            stream->Close();
            Console::WriteLine("deserialized Foo2 from Foo2");
            for each (Foo2kvp kvp in foo2c)
            {
                Console::WriteLine("{0} -> {1}", kvp.Key, kvp.Value);
            }
        }
        catch (Exception^ e)
        {
            Console::WriteLine(e);
            if (stream)
                stream->Close();
        }
    
        return 0;
    }
    

    运行此代码时,输​​出为:

    Hello World
    Deserializing Foo1
    deserialized Foo1 from Foo1
    2 -> 7
    3 -> 5
    Deserializing Foo2 from Foo1
    deserialized Foo2 from Foo1
    2 -> 7
    3 -> 5
    Deserializing Foo2
    deserialized Foo2 from Foo2
    Two -> 7
    Three -> 5
    

    遗憾的是,如果类继承自 List,则同样不起作用,因为 List&lt;T&gt; 未实现 ISerializable,因此 __super::GetObjectData 调用在派生自 List&lt;T&gt; 的类中不可用。下面的代码展示了我如何让它在一个小型应用程序中为 List 工作。

    using namespace System;
    using namespace System::IO;
    using namespace System::Collections::Generic;
    using namespace System::Runtime::Serialization;
    
    [Serializable]
    public ref class Foo1: List<int>
    { };
    
    int
    OurVersionNumber(SerializationInfo^ info)
    {
       // Serialized Foo1 has no VERSION property, but Foo2 does have it.
       // Don't use info->GetInt32("VERSION") in a try-catch statement,
       // because that is *very* slow when corresponding
       // SerializationExceptions are triggered in the debugger.
       SerializationInfoEnumerator^ it = info->GetEnumerator();
       int version = 1;
       while (it->MoveNext())
       {
          if (it->Name == "VERSION")
          {
             version = (Int32) it->Value;
             break;
          }
       }
       return version;
    }
    
    [Serializable]
    public ref class Foo2: List<String^>, ISerializable
    {
    public:
      Foo2() { }
    
      // NOTE: no "override" on this one, because List<T> doesn't provide this method
      virtual void GetObjectData(SerializationInfo^ info, StreamingContext context)
      {
        info->AddValue("VERSION", 2);
        int size = this->Count;
        List<String^>^ list = gcnew List<String^>(this);
        info->AddValue("This", list);
      }
      Foo2(SerializationInfo^ info, StreamingContext context)
      {
        int version = OurVersionNumber(info);
        if (version == 1)
        {
          int size = info->GetInt32("List`1+_size");
          array<int>^ members = (array<int>^) info->GetValue("List`1+_items", array<int>::typeid);
          for each (int value in members)
          {
            if (!size--)
              break; // done; the remaining 'members' slots are empty
            this->Add(value.ToString());
          }
          Console::WriteLine("Deserializing Foo2 from Foo1");
        }
        else
        {
          List<String^>^ list = (List<String^>^) info->GetValue("This", List<String^>::typeid);
          int size = list->Count;
          this->AddRange(list);
          size = this->Count;
          Console::WriteLine("Deserializing Foo2");
        }
      }
    };
    
    ref class MyBinder sealed: public SerializationBinder
    {
    public:
      virtual Type^ BindToType(String^ assemblyName, String^ typeName) override
      {
        if (typeName == "Foo1")
          typeName = "Foo2";
        return Type::GetType(String::Format("{0}, {1}", typeName, assemblyName));
      }
    };
    
    int main(array<System::String ^> ^args)
    {
      Console::WriteLine(L"Hello World");
      Foo1^ foo1 = gcnew Foo1;
      foo1->Add(2);
      foo1->Add(3);
    
      IFormatter^ formatter1 = gcnew Formatters::Binary::BinaryFormatter(); // no translation to Foo2
      IFormatter^ formatter2 = gcnew Formatters::Binary::BinaryFormatter();
      formatter2->Binder = gcnew MyBinder; // translate Foo1 to Foo2
      FileStream^ stream;
      try
      {
        // serialize Foo1
        stream = gcnew FileStream("fooserialized.dat", FileMode::Create, FileAccess::Write);
        formatter1->Serialize(stream, foo1);
        stream->Close();
    
        // deserialize Foo1 to Foo1
        stream = gcnew FileStream("fooserialized.dat", FileMode::Open, FileAccess::Read);
        Foo1^ foo1b = (Foo1^) formatter1->Deserialize(stream);
        stream->Close();
        Console::WriteLine("deserialized Foo1 from Foo1");
        for each (int value in foo1b)
        {
          Console::WriteLine(value);
        }
    
        // deserialize Foo1 to Foo2
        stream = gcnew FileStream("fooserialized.dat", FileMode::Open, FileAccess::Read);
        Foo2^ foo2 = (Foo2^) formatter2->Deserialize(stream);
        stream->Close();
        Console::WriteLine("deserialized Foo2 from Foo1");
        for each (String^ value in foo2)
        {
          Console::WriteLine(value);
        }
    
        // serialize Foo2
        Foo2^ foo2b = gcnew Foo2;
        foo2b->Add("Two");
        foo2b->Add("Three");
        stream = gcnew FileStream("fooserialized.dat", FileMode::Create, FileAccess::Write);
        formatter2->Serialize(stream, foo2b);
        stream->Close();
    
        // deserialize Foo2 to Foo2
        stream = gcnew FileStream("fooserialized.dat", FileMode::Open, FileAccess::Read);
        Foo2^ foo2c = (Foo2^) formatter2->Deserialize(stream);
        int size = foo2c->Count;
        stream->Close();
        Console::WriteLine("deserialized Foo2 from Foo2");
        for each (String^ value in foo2c)
        {
          Console::WriteLine(value);
        }
      }
      catch (Exception^ e)
      {
        Console::WriteLine(e);
        if (stream)
          stream->Close();
      }
    
      return 0;
    }
    

    此应用程序生成以下输出:

    Hello World
    deserialized Foo1 from Foo1
    2
    3
    Deserializing Foo2 from Foo1
    deserialized Foo2 from Foo1
    2
    3
    Deserializing Foo2
    deserialized Foo2 from Foo2
    Two
    Three
    

    但是,当在一个非常大的应用程序中使用类似的代码来反序列化旧的、深度嵌套的数据时,我不断遇到SerializationExceptions,附加信息仅限于“引用了 ID 为 number 的对象在修复中但不存在。”,并且报告的小 数字 对我来说毫无意义。检查SerializationBinder处理的类型名称显示

    System.Collections.Generic.KeyValuePair`2
    

    还有

    System.Collections.Generic.List`1
    

    所以反引号后面的数字不是固定的。这个数字是怎么确定的?如果我将其他类添加到组合中,我可以确定它不会突然改变给定类吗?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-07-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多