Как справиться с обратной совместимостью десериализации для измененных классов на основе общих коллекций?

Если старая версия приложения 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<TKey,TValue>::Dictionary' : cannot access protected member declared in class 'System::Collections::Generic::Dictionary<TKey,TValue>' with [ TKey=X ^, TValue=Y ^ ],

В более простых случаях я могу использовать info->GetValue извлекать и обрабатывать отдельные элементы данных, но в текущем случае сериализация словаря была оставлена ​​.NET (через __super::GetObjectData звоните) а я не знаю как пользоваться info->GetValue извлечь старый словарь.

Смежный вопрос: если я хочу переименовать Foo в BetterFoo и все же быть в состоянии поддерживать чтение старых сериализованных данных (по-прежнему на основе Foo), а также новые сериализованные данные (основанные на BetterFoo), тогда как мне лучше всего это сделать?

Я смотрел в SerializationBinder а также ISerializationSurrogate но не мог понять, как использовать их для решения моих проблем.

1 ответ

Я нашел частичный ответ на свои вопросы. Осмотр MemberNames а также MemberValues свойства SerializationInfo в отладчике показаны типы членов, хранящихся там. Dictionary<X^, Y^> входит в SerializationInfo как предмет с именем KeyValuePairs и введите array<System::Collections::Generic::KeyValuePair<X^, Y^>> ^, С этой информацией SerializationInfo"s GetValue Метод может быть использован для извлечения пар ключ-значение, а затем они могут быть преобразованы и добавлены к объекту, который заполняется. 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<T> не реализует ISerializable, Итак __super::GetObjectData вызов недоступен в классе, полученном из List<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

Однако при использовании аналогичного кода в очень большом приложении для десериализации старых глубоко вложенных данных я продолжаю сталкиваться с SerializationExceptions с дополнительной информацией, ограниченной "Объект с идентификационным номером был указан в исправлении, но не существует.", и сообщаемое небольшое количество не имеет смысла для меня. Проверка имен типов, обрабатываемых SerializationBinder шоу

System.Collections.Generic.KeyValuePair`2

так же как

System.Collections.Generic.List`1

таким образом, число после обратного удара не является фиксированным. Как определяется это число? Могу ли я быть уверен, что он не изменится внезапно для данного класса, если я добавлю другие классы в смесь?

Другие вопросы по тегам