Обратная совместимость in.NET с BinaryFormatter
мы используем BinaryFormatter в игре C#, чтобы сохранить ход игры пользователя, уровни игры и т. д. Мы сталкиваемся с проблемой обратной совместимости.
цели:
- Level designer создает кампанию (уровни и правила), мы меняем код, кампания должна работать нормально. Это может происходить каждый день во время разработки перед выпуском.
- пользователь сохраняет игру, мы выпускаем патч игры, пользователь все равно должен иметь возможность загружать игру
- в невидимый процесс преобразования данных должен работать независимо от того, насколько далеки две версии. Например, пользователь может пропустить наши первые 5 незначительных обновлений и получить 6-й непосредственно. Тем не менее, его сохраненные игры все равно должны загружаться нормально.
решение должно быть полностью невидимым для пользователей и дизайнеров уровней и минимально обременять кодеров, которые хотят что-то изменить (например, переименовать поле, потому что они подумали о лучшем имени).
некоторые графы объектов мы сериализовать коренятся в одном классе, в другом. Совместимость не нужна.
потенциально критические изменения (и что происходит, когда мы сериализовать и десериализовать старой версии в новую):
- добавить поле (получает инициализацию по умолчанию)
- изменить тип поля (сбой)
- переименовать поле (эквивалентно его удалению и добавлению нового)
- изменить свойство на поле и обратно (эквивалентно переименованию)
- изменение autoimplemented свойство использовать резервное поле (эквивалентно переименованию)
- добавить суперкласс (эквивалентно добавлению его полей в текущий класс)
- интерпретировать поле по-разному (например, было в градусах, теперь в радианах)
- для типов, реализующих ISerializable, мы можем изменить нашу реализацию методов ISerializable (например, начать использовать сжатие в реализации ISerializable для некоторого действительно большого типа)
- переименовать класс, переименовать перечисление значение
Я читал о:
- Версия Сериализацию
- IDeserializationCallback
- [OptionalField(VersionAdded)]
- [OnDeserializing], [OnDeserialized], [OnSerializing], [OnSerialized].
- [NotSerialized]
мое текущее решение:
- мы вносим как можно больше изменений, не нарушая, используя такие вещи, как 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
- получить все поля, которые должны быть десериализованы (аннотированы собственным атрибутом)
- заполнить объект данными из десериализованного объекта
работает со всеми нашими данными и с трех или четырех релизов.