Обратная совместимость in.NET с BinaryFormatter

мы используем BinaryFormatter в игре C#, чтобы сохранить ход игры пользователя, уровни игры и т. д. Мы сталкиваемся с проблемой обратной совместимости.

цели:

  • Level designer создает кампанию (уровни и правила), мы меняем код, кампания должна работать нормально. Это может происходить каждый день во время разработки перед выпуском.
  • пользователь сохраняет игру, мы выпускаем патч игры, пользователь все равно должен иметь возможность загружать игру
  • в невидимый процесс преобразования данных должен работать независимо от того, насколько далеки две версии. Например, пользователь может пропустить наши первые 5 незначительных обновлений и получить 6-й непосредственно. Тем не менее, его сохраненные игры все равно должны загружаться нормально.

решение должно быть полностью невидимым для пользователей и дизайнеров уровней и минимально обременять кодеров, которые хотят что-то изменить (например, переименовать поле, потому что они подумали о лучшем имени).

некоторые графы объектов мы сериализовать коренятся в одном классе, в другом. Совместимость не нужна.

потенциально критические изменения (и что происходит, когда мы сериализовать и десериализовать старой версии в новую):

  • добавить поле (получает инициализацию по умолчанию)
  • изменить тип поля (сбой)
  • переименовать поле (эквивалентно его удалению и добавлению нового)
  • изменить свойство на поле и обратно (эквивалентно переименованию)
  • изменение autoimplemented свойство использовать резервное поле (эквивалентно переименованию)
  • добавить суперкласс (эквивалентно добавлению его полей в текущий класс)
  • интерпретировать поле по-разному (например, было в градусах, теперь в радианах)
  • для типов, реализующих ISerializable, мы можем изменить нашу реализацию методов ISerializable (например, начать использовать сжатие в реализации ISerializable для некоторого действительно большого типа)
  • переименовать класс, переименовать перечисление значение

Я читал о:

мое текущее решение:

  • мы вносим как можно больше изменений, не нарушая, используя такие вещи, как OnDeserializing обратный вызов.
  • мы планируем ломать изменения на раз каждые 2 недели, поэтому меньше кода совместимости, который нужно держать вокруг.
  • каждый раз, прежде чем мы сделаем перерыв изменения, мы копируем все классы [Serializable], которые мы используем, в пространство имен/папку под названием OldClassVersions.VersionX (где X-следующий порядковый номер после последнего). Мы делаем это, даже если мы не собираемся делать релиз скоро.
  • когда пишу файл, который мы сериализуем, является экземпляром этого класса: class SaveFileData { int version; object data;}
  • при чтении из файла мы десериализуем SaveFileData и передаем его в итеративную процедуру "update", которая делает что-то вроде этого:

.

for(int i = loadedData.version; i < CurrentVersion; i++)
{
    // Update() takes an instance of OldVersions.VersionX.TheClass
    // and returns an instance of OldVersions.VersionXPlus1.TheClass
    loadedData.data = Update(loadedData.data, i);
}
  • для удобства функция Update() в своей реализации может использовать функцию CopyOverlappingPart (), которая использует отражение, чтобы скопировать как можно больше данных из старой версии в новая версия. Таким образом, функция Update() может обрабатывать только то, что действительно изменилось.

некоторые проблемы с этим:

  • десериализатор десериализуется в класс Foo, а не в класс OldClassVersions.Version5.Foo - потому что класс Foo был сериализован.
  • почти невозможно проверить или отладить
  • требуется хранить старые копии многих классов, которые подвержены ошибкам, хрупкие и раздражающие
  • Я не знайте, что делать, когда мы хотим переименовать класс

Это должно быть действительно распространенной проблемой. Как люди обычно решают эту проблему?

2 ответов


сложный вопрос. Я бы сбросил двоичный файл и использовал сериализацию XML (проще в управлении, толерантен к изменениям, которые не слишком экстремальны - например, добавление / удаление полей). В более крайних случаях проще написать преобразование (возможно, xslt) из одной версии в другую и сохранить классы чистыми. Если требуется непрозрачность и малый размер диска, вы можете попытаться сжать данные перед записью на диск.


мы получили ту же проблему в нашем приложении с хранением данных профиля пользователя (расположение столбцов сетки, настройки фильтра ...).

в нашем случае проблема заключалась в AssemblyVersion.

для этой проблемы я создаю SerializationBinder который читает фактическую версию сборки сборки (все сборки получают новый номер версии при новом развертывании) с Assembly.GetExecutingAssembly().GetName().Version.

в переопределенном методе BindToType информация о типе создается с помощью новой сборки версия.

десериализация реализуется "вручную", что означает

  • десериализовать через обычный BinaryFormatter
  • получить все поля, которые должны быть десериализованы (аннотированы собственным атрибутом)
  • заполнить объект данными из десериализованного объекта

работает со всеми нашими данными и с трех или четырех релизов.