пятница, 3 февраля 2012 г.

Про сериализацию в MFC

Сегодня про сериализацию, в частности в библиотеке MFC. Описанные ниже приемы — мой личный, опробованный на собственной шкуре опыт. О таком в MSDN, да в книгах не пишут. А зря, мль, зря… Почти все приемы выстраданы в конкретном проекте — в частности в Aml Pages. И большая их часть в ней и реализована. А некоторые нет, и я искренне весьма дюже сожалею о тяом, что допер до них слишком поздно.

На КЫВТ.ру поднимался вопрос о сериализации. Отвечал. А теперь рискну оформить наломанные мною копья в виде поста. Основной посыл этого поста, что человеку свойственно ошибаться. Ошибались, ошибаемся и ошибаться будем. И главное, всего в развитии проекта мы предвидеть не можем. Ну и собственно как можно всевозможные грабли обходить с сериализуемыми объектами.

Итак, MFC поддерживает класс CArchive для сериализации, который позволяет писать-читать из файла (хотя и не только), практически любые типы данных. Все стандартные классы MFC легко читаются\пишутся конструкциями вида CArchive<< и CArchive>>. Грабли начинаются уже потом, при развитии проекта. Именно когда объекты абстрактной софтины получают новые сериализуемые поля, или строковые данные могут быть в разных кодировках, или еще как.

1. Используйте номер версии архива

Используйте версионность CArchive. Смотреть на макрос IMLEMENT_SERIAL+VERSIONABLE_SHEMA. Суть проста: записывайте в CArchive сначала номер формата архива, а только потом сами данные. В дальнейшем это позволит сделать чтение архивов совместимым снизу вверх. Если в первой версии архива (файла), у нас записывались int+CString, а во второй int+CString+еще_int, то сериализация будет выглядеть так:

… Serialize(CArchive& ar)
{
if (CArchive::IsStoring()) {//запись архива
ar<<2;//записали номер формата — в данном случае вторая версия архива
ar<<My_int;//понеслась запись данных
ar<<myString1;
ar<<My_Int2;
}
else {//чтение
int nArchiveFormat;
ar>>nArchiveFormat;//читаем номер архива
ar>>My_int;//первый int
ar>>myString1;//первый CString
if (nArchiveFormat >=2)//если мы читаем архив второй версии, то считываем второй инт. Если архив второй или старше  версии, то читаем второй int — иначе пропускаем.
ar>>My_Int2;
}
}

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

По крайней мере, используя этот прием можно гарантировать, что новые версии программы будут корректно читать архивы, созданные старой версией программы. Конечно, старые версии программы не смогут читать архивы новых версий, но хоть совместимость снизу вверх будет обеспечена. Берем с полки пирожок.

2. Сериализуйте BOOL-переменные как битовую маску

Если в сериализуемом объекте у Вас есть набор булевых (BOOL)переменных, некоторых флагов, описывающих какое-то состояние, то не стоит их писать в архив последовательно. Не стоит писать нечто вроде CArchive<<boolFlag1<<boolFlag2<<boolFlag3; И дело тут вовсе не в экономии байтов.

Представьте себе, что в следующих версиях какие-то булевые переменные исчезнут из объекта, а какие-то добавятся. Соответственно, должен и измениться формат архива.  Вот блин, досада. И только из-за этой мелочи, как советовал выше, придется городить новый формат архива? Который не прочтут старые версии? Ан фиг! Эту проблему можно обойти. Решение простое: записывайте все флаги единой битовой маской. Нечто вроде такого:

DWORD dwBitMask=0;
enum {FLAG1=1, FLAG2=2… и.т.д.};
if (boolFlag1)
dwBitMask|=FLAG1;
if (boolFlag2)
  dwBitMask|=FLAG2;

и уже именно битовую маску dwBitMask записываем в архив. Какие плюсы? Обратите внимание, что во первых мы обошли проблему версий архивов. Появление или добавление нового флага BOOL не влияет на версию архива — все равно у нас пишется и читается DWORD.

Во-вторых, есть и еще весьма приятный бонус! Старая версия программы, которая не поддерживает каких-либо флагов из новой версии, не только корректно прочтет архив новой версии, но и запишет его обратно неизменным. Для этого необходимо, чтобы битовые маски не создавались и парсились на этапе записи\чтения, а именно в виде подобной маски и хранились в объекте. Тогда старая версия всегда одинаково прочтет битовую маску. Но булевый флаг, который появился у нас в новой версии она оставит неизменным. Как считает, так и запишет.

Согласитесь, движение в сторону совместимости сверху вниз это уже неплохо. У этого приема есть пара деталей, но это уже подробности и если нужно опишу их в другом посте. Как использовать битовую маску для хранения в объекте в runtime писать не буду — не об том пост.

3. Не сериализуйте строки как есть

Не реализуйте сериализацию строк, как предлагает в примерах по MFC. Не используйте в лоб запись и чтение вида CArchive<<myString и CArchive>>myString. Поясню почему. CString в MFC инкапусулирует или Unicode-строку, или ANSI-строку — в зависимости от настроек компиляции (MBCS|Unicode). Поэтому стандартная сериализация полностью полагается на внутренности CString. А оно нам надо? Всё, робяты! 90-ые давно прошли — на дворе 21-век. Нет просто строк, есть последовательности с завершающим нулем. Но вот в какой кодировке могут быть конкретные строки — выбирай на вкус. Хотите ANSI, хотите UTF8, хотите Unicode.

Простой случай: а допустим в CString у нас хранится HTML-код… HTML-код… Он такой… Он вообще может быть в какой угодно кодировке. Так что имеет смысл делать сериализацию CString ручками, записывать и кодировку, и длину, и только потом непосредственно поток байтов. Зато потом прочтете без проблем, в любой версии софтины. Поверьте на слово, то что кажется неважным в начале проекта, потом в развитии может вызвать очень большую головную боль.

Ну вот, собственно, некоторые несложные приемы сериализации. Есть конечно и еще детали, и еще подробности. А деталей хватает. Начиная от корректной реакции софта на попытку прочтения новой версии архива, и заканчивая, ситуцией, когда MFC-программа, скомпилированная как MBCS читает архив записанной UNICODE версией, или наоборот. Есть там нюансы, и именно в них весь дьявол. Но о них как-нибудь в другом посте.

Комментариев нет:

Отправить комментарий