Как справиться с обратной совместимостью десериализации для измененных классов на основе общих коллекций?
Если старая версия приложения 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
Однако при использовании аналогичного кода в очень большом приложении для десериализации старых глубоко вложенных данных я продолжаю сталкиваться с SerializationException
s с дополнительной информацией, ограниченной "Объект с идентификационным номером был указан в исправлении, но не существует.", и сообщаемое небольшое количество не имеет смысла для меня. Проверка имен типов, обрабатываемых SerializationBinder
шоу
System.Collections.Generic.KeyValuePair`2
так же как
System.Collections.Generic.List`1
таким образом, число после обратного удара не является фиксированным. Как определяется это число? Могу ли я быть уверен, что он не изменится внезапно для данного класса, если я добавлю другие классы в смесь?