如何处理基于泛型集合的已更改类的反序列化向后兼容性?

How to handle deserialization backward compatibility for changed classes based on generic collections?

提问人:Louis Strous 提问时间:1/25/2012 最后编辑:LU RDLouis Strous 更新时间:1/26/2012 访问量:850

问:

如果旧版本的 c++/CLI 应用程序序列化了一个从 Dictionary 派生的类,涉及 类型的键,而新版本需要将键类型更改为 ,那么我怎样才能最好地使应用程序支持读取旧的序列化数据(仍然基于)以及新的序列化数据(基于)?FooXZXZ

如果旧情况是这样的:

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<TKey,TValue>::Dictionary' : cannot access protected member declared in class 'System::Collections::Generic::Dictionary<TKey,TValue>' with [ TKey=X ^, TValue=Y ^ ].

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

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

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

.NET C++-CLI 反序列化 向后兼容性

评论


答:

1赞 Louis Strous 1/26/2012 #1

我已经找到了我自己问题的部分答案。检查调试器中 的 和 属性将显示存储在其中的成员的类型。A 包含在 中,作为具有名称和类型的项目。有了这些信息,就可以使用 ' 方法来检索键值对,然后可以转换它们并将其添加到要填充的对象中。A 可用于让一个类的反序列化构造函数处理另一个类的反序列化,从而允许在重命名类后向后兼容。下面的代码显示了所有这些内容。MemberNamesMemberValuesSerializationInfoDictionary<X^, Y^>SerializationInfoKeyValuePairsarray<System::Collections::Generic::KeyValuePair<X^, Y^>> ^SerializationInfoGetValueSerializationBinder

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

遗憾的是,如果类继承自 ,则同样不起作用,因为没有实现 ,因此该调用在派生自 的类中不可用。下面的代码显示了我如何让它在一个小应用程序中工作。ListList<T>ISerializable__super::GetObjectDataList<T>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

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

System.Collections.Generic.KeyValuePair`2

以及

System.Collections.Generic.List`1

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