Cериализация для начинающих - Часть 3
ОГЛАВЛЕНИЕ
Данная статья является третьей частью руководства по сериализации, состоящего из трех частей.
- Часть 1 является введением в основы сериализации.
- Часть 2 объясняет, как обрабатывать чтение данных из неисправных хранилищ данных и поддерживать контроль версий.
- Часть 3 описывает, как переводить в последовательную форму сложные объекты.
Сериализация производного класса
В данном случае производный класс является производным от простого класса и не является коллекцией. Для сериализации производного класса нужно сделать следующее:
- Сериализовать сигнатуру и версию объекта
- Сериализовать базовый класс объекта << дополнительный шаг
- Сериализовать элементы (члены) объекта (если они есть)
В следующем примере класс ColoredPoint является производным от класса Point и содержит дополнительный целочисленный член (int), названный m_nColor, который определяет цвет точки. Как и все упорядочиваемые классы, ColoredPoint также определяет статическую сигнатуру и версию.
int ColoredPoint::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
// Шаг 1: Сериализация сигнатуры и версии
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ColoredPoint::m_strSignature;
(*pArchive) << ColoredPoint::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ColoredPoint::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ColoredPoint::m_nVersion;)
return (Status::UnsupportedVersion);
}
// Шаг 2: Сериализация базового класса
int nStatus = Point::serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
// Шаг 3: Сериализация членов
if (pArchive->IsStoring())
(*pArchive) << m_nColor;
else
(*pArchive) >> m_nColor;
}
catch (CException* pException) {
// Произошла ошибка чтения/записи
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
// Объект был успешно сериализован (упорядочен)
return (Status::Success);
}
Сериализация однородной коллекции
Однородные коллекции используются для хранения коллекций с динамически изменяемым размером, содержащих объекты одинакового типа. Для сериализации однородной коллекции нужно сделать следующее:
- Сериализовать сигнатуру и версию объекта
- Сериализовать базовый класс объекта (если он есть)
- Сериализовать число элементов в коллекции << дополнительный шаг
- Сериализовать каждый объект в коллекции << дополнительный шаг
- Сериализовать элементы (члены) объекта (если они есть)
В следующем примере класс ColoredPointList является коллекцией объектов ColoredPoint. Для простоты ColoredPointList использует CPtrArray для хранения объектов. Как и все упорядочиваемые классы, ColoredPointList также определяет статическую сигнатуру и версию. Здесь представлено как ColoredPointList: выглядит
class ColoredPointList
{
// Конструктор/Деструктор
public:
ColoredPointList::ColoredPointList();
virtual ColoredPointList::~ColoredPointList()
// Атрибуты
public:
static const CString m_strSignature;
static const int m_nVersion;
// Операции
public:
int serialize (CArchive* pArchive);
// Члены
protected:
CPtrArray m_coloredPoints; }
Здесь показана его сериализация:
int ColoredPointList::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
int nStatus = Status::Success;
// Шаг 1: Сериализация сигнатуры и версии
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ColoredPointList::m_strSignature;
(*pArchive) << ColoredPointList::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ColoredPointList::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ColoredPointList::m_nVersion;)
return (Status::UnsupportedVersion);
}
// Шаг 2: Сериализация базового класса (если он есть)
//
// Ничего не выполняется, так как ColoredPointList не является производным классом.
// Но если бы он был произведен от BaseColoredPointList, то выполнялся бы ряд действий:
//
// nStatus = BaseColoredPointList::serialize (pArchive);
// if (nStatus != Status::Success)
// return (nStatus);
// Шаг 3: Сериализация числа элементов в коллекции
int nItems = 0;
if (pArchive->IsStoring()) {
nItems = m_coloredPoints.GetSize();
(*pArchive) << nItems;
} else
(*pArchive) >> nItems;
// Шаг 4: Сериализация каждого объекта в коллекции
for (int nObject=0; (nObject < nItems); nObject++) {
// 4a: Сериализация указателя на объект
ColoredPoint* pColoredPoint = NULL;
if (pArchive->IsStoring())
pColoredPoint = (ColoredPoint *) m_coloredPoints.GetAt (nObject);
else
pColoredPoint = new ColoredPoint();
ASSERT (pColoredPoint != NULL);
// 4b: Сериализация объекта
nStatus = pColoredPoint->serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
if (!pArchive->IsStoring())
m_coloredPoints.Add (pColoredPoint);
}
// Шаг 5: Сериализация других элементов объекта (если они есть)
//
// Ничего не делается, так как ColoredPointList не имеет никаких других элементов. Но если бы он содержал целочисленный элемент (m_nSomeInt) и объект Foo
// (m_foo), выполнялись бы следующие действия:
//
// if (pArchive->IsStoring())
// (*pArchive) << m_nSomeInt;
// else
// (*pArchive) >> m_nColor;
//
// nStatus = m_foo::serialize (pArchive);
// if (nStatus != Status::Success)
// return (nStatus);
}
catch (CException* pException) {
// Произошла ошибка чтения/записи
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
// Объект был успешно сериализован (упорядочен)
return (Status::Success);
}
Сериализация разнородной коллекции
Разнородные коллекции используются для хранения коллекций динамически изменяемых размеров, содержащих потенциально различные типы объектов. Для сериализации разнородной коллекции необходимо сделать следующее:
- Сериализовать сигнатуру и версию объекта
- Сериализовать базовый класс объекта (если он есть)
- Сериализовать число элементов в коллекции
- Для каждого объекта в коллекции
(a) сериализовать сигнатуру этого объекта << дополнительный шаг
(b) затем сериализовать объект - Сериализовать другие элементы объекта (если они есть)
Дополнительным шагом в сериализации разнородных коллекций является 4(a), где сериализуется сигнатура каждого объекта перед сериализацией самого объекта. Это оказывается полезным при обратном считывании данных. При сериализации однородной коллекции приходится иметь дело с объектами одинакового типа (ColoredPoint в предыдущем примере). Для чтения ColoredPoint этот класс создается в области кучи, и затем вызывается его метод serialize().
ColoredPoint* pColoredPoint = new ColoredPoint();
nStatus = pColoredPoint->serialize (pArchive);
Для разнородных коллекций необходимо знать тип каждого считываемого в обратном направлении объекта, перед тем как его сериализовать. Здесь и нужно использовать сигнатуру объекта. Поскольку сигнатура была сохранена на выходе, можно создать объект соответствующего типа на входе.
// Чтение сигнатуры объекта
CString strSignature;
pArchive >> strSignature;
// Создание объекта соответствующего типа
ISerializable* pObject = NULL;
if (strSignature == ColoredPoint::m_strSignature)
pObject = new ColoredPoint();
else
if (strSignature == Line::m_strSignature)
pObject = new Line();
else
if (strSignature == Rectangle::m_strSignature)
pObject = new Rectangle();
else
return (Status::InvalidFormat);
ASSERT (pObject != NULL);
// Чтение из объекта в обратном направлении
nStatus = pObject->serialize (pArchive);
В приведенном выше фрагменте кода ColoredPoint, Line и Rectangle - все являются производными от общего базового класса ISerializable, который является абстрактным базовым классом, содержащим только чисто виртуальные методы (другими словами, "интерфейс"). ISerializable определяет методы getSignature(), getVersion() и serialize().
class ISerializable
{
// Конструктор/деструктор
public:
ISerializable::ISerializable()
{ }
virtual ISerializable::~ISerializable()
{ }
// Методы
public:
// Получение сигнатуры объекта
virtual CString getSignature() = 0;
// Получение версии объекта
virtual int getVersion() = 0;
// Сериализация объекта
virtual int serialize (CArchive* pArchive) = 0;
}
Теперь выполним сериализацию разнородной коллекции. В следующем примере класс ShapeList является коллекцией различных количеств объектов ColoredPoint, Line и Rectangle, все из которых являются производными от ISerializable. На эти классы можно смотреть как на "реализующие интерфейс ISerializable".
int ShapeList::serialize
(CArchive* pArchive)
{
ASSERT (pArchive != NULL);
int nStatus = Status::Success;
// Шаг 1: Сериализация сигнатуры и версии
int nVersion;
try {
if (pArchive->IsStoring()) {
(*pArchive) << ShapeList::m_strSignature;
(*pArchive) << ShapeList::m_nVersion;
} else {
CString strSignature;
(*pArchive) >> strSignature;
if (strSignature != ShapeList::m_strSignature)
return (Status::InvalidFormat);
(*pArchive) >> nVersion;
if (nVersion > ShapeList::m_nVersion;)
return (Status::UnsupportedVersion);
}
// Шаг 2: Сериализация базового класса (если он есть)
//
// Ничего не выполняется, так как ShapeList не является производным классом.
// Но если бы он был производным от BaseShapeList, выполнялись бы следующие действия:
//
// nStatus = BaseShapeList::serialize (pArchive);
// if (nStatus != Status::Success)
// return (nStatus);
// Шаг 3: Сериализация числа элементов в коллекции
int nItems = 0;
if (pArchive->IsStoring()) {
nItems = m_shapes.GetSize();
(*pArchive) << nItems;
} else
(*pArchive) >> nItems;
// Шаг 4: Сериализация каждого элемента в коллекции
for (int nObject=0; (nObject < nItems); nObject++) {
// 4a: Сначала выполняется сериализация сигнатуры объекта
CString strSignature;
if (pArchive->IsStoring())
(*pArchive) << pObject->getSignature();
else
(*pArchive) >> strSignature;
//
// 4b: Затем сериализуется объект
//
// 4b (1): Указатель на сериализуемый объект
ISerializable* pObject = NULL;
if (pArchive->IsStoring())
pObject = (ISerializable *) m_shapes.GetAt (nObject);
else {
if (strSignature == ColoredPoint::m_strSignature)
pObject = new ColoredPoint();
else
if (strSignature == Line::m_strSignature)
pObject = new Line();
else
if (strSignature == Rectangle::m_strSignature)
pObject = new Rectangle();
else
return (Status::InvalidFormat);
}
ASSERT (pObject != NULL);
// 4b (2): Сериализация объекта
nStatus = pObject->serialize (pArchive);
if (nStatus != Status::Success)
return (nStatus);
if (!pArchive->IsStoring())
m_shapes.Add (pObject);
}
// Шаг 5: Сериализация других элементов объекта (если они есть)
//
// Ничего не делается, так как ShapeList не имеет никаких других членов. Но если бы он содержал целое число (m_nSomeInt) и
// объект Foo (m_foo), выполнялись бы следующие действия:
//
// if (pArchive->IsStoring())
// (*pArchive) << m_nSomeInt;
// else
// (*pArchive) >> m_nColor;
//
// nStatus = m_foo::serialize (pArchive);
// if (nStatus != Status::Success)
// return (nStatus);
}
catch (CException* pException) {
// Произошла ошибка чтения/записи
pException->Delete();
if (pArchive->IsStoring())
return (Status::WriteError);
return (Status::ReadError);
}
// Объект был успешно сериализован (упорядочен)
return (Status::Success);
}
Фабрика класса
Вы можете заменить условный оператор во фрагменте кода, используя "фабрику класса" для создания нового члена класса на основе его сигнатуры.
Фабрика класса - это немного больше чем класс, предоставляющий соответственно названный статический метод (например: create()), который создает объект определенного типа. Вы можете скрыть условный оператор в методе фабрики create(), немного приведя код в порядок..
...
// Создание объекта соответствующего типа
ISerializable* pObject = MyClassFactory::create (strSignature);
ASSERT (pObject != NULL);
...
Заключение
Хотя этот пример ссылается на объекты MFC, такие как CString и CPtrArray, сериализация не относится только к MFC. Это обычное действие, выполняемое любым программным обеспечением, которому требуется сохранять и восстанавливать информацию в и из постоянного хранилища.
Автор: Ravi Bhavnani
