TN002: Formato de datos de objetos persistentes
En esta nota, se describen las rutinas de MFC que admiten objetos de C++ persistentes y el formato de los datos de objetos cuando se almacenan en un archivo. Esto solo se aplica a las clases con las macros DECLARE_SERIAL y IMPLEMENT_SERIAL.
El problema
La implementación de MFC para los datos de almacenes de datos persistentes de muchos objetos en una sola parte contigua de un archivo. El método Serialize
del objeto traduce los datos del objeto a un formato binario compacto.
La implementación garantiza que todos los datos se guarden en el mismo formato mediante la clase CArchive. Usa un objeto CArchive
como traductor. Este objeto persiste desde el momento en que se crea hasta que se llama a CArchive::Close. El programador puede llamar a este método de manera explícita, o bien el destructor puede llamarlo implícitamente cuando el programa sale del ámbito que contiene CArchive
.
En esta nota, se describe la implementación de los miembros CArchive
CArchive::ReadObject y CArchive::WriteObject. Encontrará el código de estas funciones en Arcobj.cpp y la implementación principal de CArchive
en Arccore.cpp. El código de usuario no llama directamente a ReadObject
ni a WriteObject
. En su lugar, estos objetos se usan mediante operadores de inserción y extracción con seguridad de tipos específicos de clase generados automáticamente por las macros DECLARE_SERIAL y IMPLEMENT_SERIAL. En el código siguiente, se muestra cómo se llama implícitamente a WriteObject
y ReadObject
:
class CMyObject : public CObject
{
DECLARE_SERIAL(CMyObject)
};
IMPLEMENT_SERIAL(CMyObj, CObject, 1)
// example usage (ar is a CArchive&)
CMyObject* pObj;
CArchive& ar;
ar <<pObj; // calls ar.WriteObject(pObj)
ar>> pObj; // calls ar.ReadObject(RUNTIME_CLASS(CObj))
Guardado de objetos en el almacén (CArchive::WriteObject)
El método CArchive::WriteObject
escribe datos de encabezado que se usan para reconstruir el objeto. Estos datos constan de dos partes: el tipo del objeto y el estado del objeto. Este método también es responsable de mantener la identidad del objeto que se está escribiendo, de modo que solo se guarde una copia única, independientemente del número de punteros a ese objeto (incluidos los punteros circulares).
Guardar (insertar) y restaurar (extraer) objetos se basa en varias "constantes de manifiesto". Estos son valores almacenados en binarios y que proporcionan información importante al archivo (tenga en cuenta que el prefijo "w" indica cantidades de 16 bits):
Etiqueta | Descripción |
---|---|
wNullTag | Se usa para punteros de objetos NULL (0). |
wNewClassTag | Indica que la descripción de la clase que sigue es nueva en este contexto de archivo (-1). |
wOldClassTag | Indica que la clase del objeto que se lee se ha visto en este contexto (0x8000). |
Al almacenar objetos, el archivo mantiene un CMapPtrToPtr (el m_pStoreMap), que es una asignación de un objeto almacenado a un identificador persistente (PID) de 32 bits. Se asigna un PID a cada objeto único y cada nombre de clase única que se guarda en el contexto del archivo. Estos PID se entregan de manera secuencial a partir de 1. Estos PID no tienen importancia fuera del ámbito del archivo y, en particular, no deben confundirse con números de registro u otros elementos de identidad.
En la clase CArchive
, los PID son de 32 bits, pero se escribe como de 16 bits a menos que sean mayores que 0x7FFE. Los PID grandes se escriben como 0x7FFF seguido del PID de 32 bits. Esto mantiene la compatibilidad con proyectos creados en versiones anteriores.
Cuando se realiza una solicitud para guardar un objeto en un archivo (por lo general, mediante el operador de inserción global), se realiza una comprobación para un puntero CObject NULL. Si el puntero es NULL, wNullTag se inserta en el flujo de archivo.
Si el puntero no es NULL y se puede serializar (la clase es una clase DECLARE_SERIAL
), el código comprueba m_pStoreMap para ver si ya se guardó el objeto. Si es así, el código inserta el PID de 32 bits asociado con ese objeto en el flujo de archivo.
Si el objeto no se guardó antes, hay dos posibilidades que es necesario tener en cuenta: tanto el objeto como el tipo exacto (es decir, la clase) del objeto son nuevos en este contexto de archivo, o bien el objeto es de un tipo exacto ya visto. A fin de determinar si se ha visto el tipo, el código consulta m_pStoreMap en busca de un objeto CRuntimeClass que coincida con el objeto CRuntimeClass
asociado con el objeto que se guarda. Si hay una coincidencia, WriteObject
inserta una etiqueta que es OR
bit a bit de wOldClassTag y este índice. Si CRuntimeClass
es nuevo en este contexto de archivo, WriteObject
asigna un PID nuevo a esa clase y lo inserta en el archivo, precedido del valor wNewClassTag.
A continuación, el descriptor de esta clase se inserta en el archivo mediante el método CRuntimeClass::Store
. CRuntimeClass::Store
inserta el número de esquema de la clase (ver a continuación) y el nombre de texto ASCII de la clase. Tenga en cuenta que el uso del nombre de texto ASCII no garantiza que el archivo sea único entre distintas aplicaciones. Por lo tanto, debe etiquetar los archivos de datos para evitar cualquier daño. Después de la inserción de la información de clase, el archivo coloca el objeto en m_pStoreMap y, luego, llama al método Serialize
para insertar datos específicos de la clase. Colocar el objeto en m_pStoreMap antes de llamar a Serialize
evita que se guarden varias copias del objeto en el almacén.
Al volver al autor de llamada inicial (por lo general, la raíz de la red de objetos), debe llamar a CArchive::Close. Si tiene previsto realizar otras operaciones CFile, debe llamar al método CArchive
Flush para evitar dañar el archivo.
Nota:
Esta implementación impone un límite máximo de 0x3FFFFFFE índices por contexto de archivo. Este número representa el número máximo de clases y objetos únicos que se pueden guardar en un solo archivo, pero un archivo de disco único puede tener un número ilimitado de contextos de archivo.
Carga de objetos desde el almacén (CArchive::ReadObject)
La carga (extracción) de objetos usa el método CArchive::ReadObject
y es lo contrario de WriteObject
. Al igual que lo que ocurre con WriteObject
, el código de usuario no llama directamente a ReadObject
; el código de usuario debe llamar al operador de extracción con seguridad de tipos que llama a ReadObject
con el CRuntimeClass
esperado. Esto garantiza la integridad de tipo de la operación de extracción.
Como la implementación WriteObject
asignaba PID crecientes, a partir de 1 (0 está predefinido como el objeto NULL), la implementación ReadObject
puede usar una matriz para mantener el estado del contexto de archivo. Cuando se lee un PID desde el almacén, si el PID es mayor que el límite superior actual de m_pLoadArray, ReadObject
sabe que sigue un objeto nuevo (o descripción de clase).
Números de esquema
El número de esquema, que se asigna a la clase cuando se encuentra el método IMPLEMENT_SERIAL
de la clase, es la "versión" de la implementación de la clase. El esquema hace referencia a la implementación de la clase, no al número de veces que se ha hecho persistente un objeto determinado (por lo general, denominado "versión del objeto").
Si tiene previsto mantener varias implementaciones diferentes de la misma clase a lo largo del tiempo, incrementar el esquema a medida que revisa la implementación del método Serialize
del objeto le permitirá escribir código que puede cargar objetos almacenados mediante el uso de versiones anteriores de la implementación.
El método CArchive::ReadObject
generará una excepción CArchiveException cuando se encuentre un número de esquema en el almacén persistente que difiera del número de esquema de la descripción de clase en memoria. No es fácil recuperarse de esta excepción.
Puede usar VERSIONABLE_SCHEMA
en combinación con (OR bit a bit) la versión del esquema para evitar que se genere esta excepción. Al usar VERSIONABLE_SCHEMA
, el código puede realizar la acción adecuada en su función Serialize
comprobando el valor devuelto de CArchive::GetObjectSchema.
Llamada directa a la serialización
En muchos casos, no es necesario sobrecargar el esquema de archivo de objetos general de WriteObject
y ReadObject
. Este es el caso común de serializar los datos en un CDocument. En este caso, se llama directamente al método Serialize
de CDocument
, no con los operadores de extracción o inserción. A su vez, el contenido del documento puede utilizar el esquema de archivo de objetos más general.
Llamar directamente a Serialize
tiene estas ventajas y desventajas:
No se agregan bytes adicionales al archivo antes ni después de serializar el objeto. Esto no solo hace que los datos guardados sean más pequeños, sino que permite implementar rutinas
Serialize
que pueden controlar cualquier formato de archivo.MFC está optimizado, por lo que las implementaciones
WriteObject
yReadObject
y las colecciones relacionadas no se vincularán a la aplicación, a menos que necesite el esquema de archivo de objetos más general para algún otro propósito.El código no tiene que recuperarse de números de esquema antiguos. Esto hace que el código de serialización del documento sea responsable de codificar números de esquema, números de versión de formato de archivo o cualquier número de identificación que use al principio de los archivos de datos.
Cualquier objeto que se serialice con una llamada directa a
Serialize
no debe usarCArchive::GetObjectSchema
o debe controlar un valor devuelto de (UINT)-1 que indique que la versión era desconocida.
Dado que se llama a Serialize
directamente en el documento, no suele ser posible que los objetos secundarios del documento archiven las referencias a su documento primario. Se debe conceder a estos objetos un puntero a su documento contenedor de manera explícita, o bien se debe usar la función CArchive::MapObject para asignar el puntero CDocument
a un PID antes de que se archiven estos punteros posteriores.
Como se indicó anteriormente, debe codificar la información de versión y de clase usted mismo al llamar directamente a Serialize
, lo que le permite cambiar el formato más adelante y mantener la compatibilidad con versiones anteriores con archivos más viejos. Es posible llamar de manera explícita a la función CArchive::SerializeClass
antes de serializar directamente un objeto o antes de llamar a una clase base.