TN002: formato de dados do objeto persistente

Esta observação descreve as rotinas do MFC que dão suporte a objetos persistentes do C++ e o formato dos dados do objeto quando são armazenados em um arquivo. Isso se aplica apenas a classes com as macros DECLARE_SERIAL e IMPLEMENT_SERIAL.

O problema

A implementação do MFC para dados persistentes armazena dados para muitos objetos em uma única parte contígua de um arquivo. O método Serialize do objeto converte os dados do objeto em um formato binário compacto.

A implementação garante que todos os dados sejam salvos no mesmo formato, usando a Classe CArchive. Ela usa um objeto CArchive como tradutor. Esse objeto persiste desde o momento em que é criado até que você chame CArchive::Close. Esse método pode ser chamado explicitamente pelo programador ou implicitamente pelo destruidor, quando o programa sai do escopo que contém o CArchive.

Esta observação descreve a implementação dos membros CArchiveCArchive::ReadObject e CArchive::WriteObject. Você encontrará o código dessas funções em Arcobj.cpp e a implementação principal para CArchive em Arccore.cpp. O código do usuário não chama ReadObject e WriteObject diretamente. Em vez disso, esses objetos são usados por operadores de extração e inserção type-safe específicos da classe, que são gerados automaticamente pelas macros DECLARE_SERIAL e IMPLEMENT_SERIAL. O código a seguir mostra como WriteObject e ReadObject são implicitamente chamados:

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))

Como salvar os objetos no repositório (CArchive::WriteObject)

O método CArchive::WriteObject grava os dados de cabeçalho usados para reconstruir o objeto. Esses dados consistem em duas partes: o tipo do objeto e o estado do objeto. Esse método também é responsável por manter a identidade do objeto que está sendo gravado, de modo que apenas uma única cópia seja salva, independentemente do número de ponteiros para esse objeto (incluindo ponteiros circulares).

Salvar (inserir) e restaurar (extrair) objetos depende de várias "constantes de manifesto". Esses são valores armazenados em binário e fornecem informações importantes para a camada de arquivos (observe que o prefixo "w" indica as quantidades de 16 bits):

Marca Descrição
wNullTag Usado para ponteiros de objeto NULL (0).
wNewClassTag Indica que a descrição da classe a seguir é nova nesse contexto de camada de arquivos (-1).
wOldClassTag Indica que a classe do objeto lido foi vista nesse contexto (0x8000).

Ao armazenar objetos, a camada de arquivos mantém um CMapPtrToPtr (o m_pStoreMap), que é um mapeamento de um objeto armazenado para um PID (identificador persistente) de 32 bits. Um PID é atribuído a cada objeto exclusivo e a cada nome de classe exclusivo salvo no contexto da camada de arquivos. Esses PIDs são entregues sequencialmente a partir de 1. Esses PIDs não têm significado fora do escopo da camada de arquivos e, especificamente, não devem ser confundidos com os números de registro ou outros itens de identidade.

Na classe CArchive, os PIDs têm 32 bits, mas são gravados como se tivessem 16 bits, a menos que sejam maiores que 0x7FFE. Os PIDs grandes são gravados como 0x7FFF seguidos pelo PID de 32 bits. Isso mantém a compatibilidade com projetos criados em versões anteriores.

Quando uma solicitação é feita para salvar um objeto em uma camada de arquivos (geralmente usando o operador de inserção global), uma verificação é feita para um ponteiro CObject NULL. Se o ponteiro for NULL, o wNullTag será inserido no fluxo de camada de arquivos.

Se o ponteiro não for NULL e puder ser serializado (essa é uma classe DECLARE_SERIAL), o código verificará o m_pStoreMap para ver se o objeto já foi salvo. Nesse caso, o código inserirá o PID de 32 bits associado a esse objeto no fluxo de camada de arquivos.

Se o objeto não tiver sido salvo anteriormente, há duas possibilidades a serem consideradas: tanto o objeto quanto o tipo exato (ou seja, classe) do objeto são novos nesse contexto de camada de arquivos ou o objeto é de um tipo exato já visto. Para determinar se o tipo foi visto, o código consulta o m_pStoreMap para um objeto CRuntimeClass que corresponde ao objeto CRuntimeClass associado ao objeto que está sendo salvo. Se houver uma correspondência, WriteObject insere uma marca que é o OR bit a bit do wOldClassTag e esse índice. Se o CRuntimeClass for novo nesse contexto de camada de arquivos, WriteObject atribui um novo PID a essa classe e o insere na camada de arquivos, precedido pelo valor wNewClassTag.

O descritor dessa classe é inserido na camada de arquivos, usando o método CRuntimeClass::Store. CRuntimeClass::Store insere o número de esquema da classe (veja abaixo) e o nome de texto do ASCII da classe. Observe que o uso do nome de texto do ASCII não garante a exclusividade da camada de arquivos entre aplicativos. Portanto, você deve marcar os arquivos de dados para evitar que sejam corrompidos. Após a inserção das informações de classe, a camada de arquivos coloca o objeto no m_pStoreMap e, em seguida, chama o método Serialize para inserir dados específicos da classe. Colocar o objeto no m_pStoreMap antes de chamar Serialize impede que várias cópias do objeto sejam salvas no repositório.

Ao retornar ao chamador inicial (geralmente a raiz da rede de objetos), você deve chamar CArchive::Close. Se você planeja executar outras operações CFile, deve chamar o método CArchiveFlush para evitar que a camada de arquivos seja corrompida.

Observação

Essa implementação impõe um limite rígido de índices de 0x3FFFFFFE por contexto de camada de arquivos. Esse número representa o número máximo de objetos e classes exclusivos que podem ser salvos em uma única camada de arquivos, mas um único arquivo de disco pode ter um número ilimitado de contextos de camada de arquivos.

Carregamento de Objetos no Repositório (CArchive::ReadObject)

O carregamento (a extração) de objetos usa o método CArchive::ReadObject e é o inverso de WriteObject. Assim como acontece com WriteObject, ReadObject não é chamado diretamente pelo código do usuário. O código do usuário deve chamar o operador de extração type-safe, que chama ReadObject com o CRuntimeClass esperado. Isso garante a integridade do tipo da operação de extração.

Como a implementação WriteObject atribuiu PIDs crescentes, a partir de 1 (0 é predefinido como o objeto NULL), a implementação ReadObject pode usar uma matriz para manter o estado do contexto de camada de arquivos. Quando um PID é lido no repositório, se o PID for maior que o limite superior atual do m_pLoadArray, ReadObject saberá que um novo objeto (ou descrição de classe) seguirá.

Números de Esquema

O número de esquema, que é atribuído à classe quando o método IMPLEMENT_SERIAL da classe é encontrado, é a "versão" da implementação da classe. O esquema se refere à implementação da classe, e não ao número de vezes que determinado objeto se tornou persistente (geralmente chamado de versão do objeto).

Se você pretende manter várias implementações diferentes da mesma classe ao longo do tempo, incrementar o esquema à medida que você examina a implementação do método Serialize do objeto permitirá que você grave um código que pode carregar objetos armazenados, usando as versões mais antigas da implementação.

O método CArchive::ReadObject gerará um CArchiveException, quando encontrar um número de esquema no repositório persistente que seja diferente do número de esquema da descrição da classe na memória. Não é fácil se recuperar dessa exceção.

Você pode usar o VERSIONABLE_SCHEMA combinado com (OR bit a bit) a versão de esquema para impedir que essa exceção seja gerada. Ao usar o VERSIONABLE_SCHEMA, o código pode executar a ação apropriada na função Serialize, verificando o valor retornado de CArchive::GetObjectSchema.

Como chamar a serialização diretamente

Em muitos casos, a sobrecarga do esquema geral de camada de arquivos do objeto de WriteObject e ReadObject não é necessária. Esse é o caso comum de serializar os dados em um CDocument. Nesse caso, o método Serialize do CDocument é chamado diretamente, e não com os operadores de extração ou inserção. O conteúdo do documento pode, por sua vez, usar o esquema mais geral de camada de arquivos do objeto.

Chamar Serialize diretamente tem as seguintes vantagens e desvantagens:

  • Bytes extras não são adicionados à camada de arquivos, antes ou depois que o objeto é serializado. Isso não só faz com que os dados salvos sejam menores, como também permite implementar rotinas Serialize que possam lidar com qualquer formato de arquivo.

  • O MFC é ajustado para que as implementações WriteObject e ReadObject, bem como as coleções relacionadas não sejam vinculadas ao aplicativo, a menos que você precise do esquema mais geral de camada de arquivos do objeto para alguma outra finalidade.

  • O código não precisa se recuperar dos números de esquema antigos. Isso torna o código de serialização do documento responsável por codificar números de esquema, números de versão de formato de arquivo ou quaisquer números de identificação usados no início dos arquivos de dados.

  • Qualquer objeto serializado com uma chamada direta para Serialize não deve usar CArchive::GetObjectSchema ou deve lidar com um valor retornado de (UINT)-1, indicando que a versão era desconhecida.

Como o Serialize é chamado diretamente no documento, normalmente não é possível que os sub-objetos do documento arquivem referências ao documento pai. Esses objetos devem receber um ponteiro para o documento de contêiner explicitamente ou você deve usar a função CArchive::MapObject para mapear o ponteiro CDocument para um PID, antes que esses ponteiros traseiros sejam arquivados.

Conforme observado anteriormente, você deve codificar as informações de versão e classe ao chamar o Serialize diretamente, permitindo que você altere o formato mais tarde, mantendo a compatibilidade com os arquivos anteriores. A função CArchive::SerializeClass pode ser chamada explicitamente, antes de serializar diretamente um objeto ou antes de chamar uma classe base.

Confira também

Observações técnicas por número
Observações técnicas por categoria