Partilhar via


TN002: Formato de dados de objeto persistente

Esta nota descreve as rotinas MFC que suportam objetos C++ persistentes 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 MFC para dados persistentes armazena dados para muitos objetos em uma única parte contígua de um arquivo. O método do Serialize objeto traduz 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. Ele usa um CArchive objeto como um tradutor. Este objeto persiste a partir do momento em que é criado até chamar CArchive::Close. Este método pode ser chamado explicitamente pelo programador ou implicitamente pelo destruidor quando o programa sai do escopo que contém o CArchive.

Esta nota descreve a CArchive implementação dos membros CArchive::ReadObject e CArchive::WriteObject. Você encontrará o código para essas funções em Arcobj.cpp, e a implementação principal para CArchive em Arccore.cpp. O código do utilizador não chama ReadObject e WriteObject diretamente. Em vez disso, esses objetos são usados por operadores de inserção e extração seguros de tipo específicos de 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))

Salvando objetos no repositório (CArchive::WriteObject)

O método CArchive::WriteObject grava dados de cabeçalho que são 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 manifestas". Estes são valores que são armazenados em formato binário e fornecem informações importantes para o arquivo (note que o prefixo "w" indica quantidades de 16 bits):

Etiqueta Descrição
wNullTag Usado para ponteiros de objeto NULL (0).
wNewClassTag Indica que a descrição da classe a seguir é nova neste contexto de arquivo (-1).
wOldClassTag Indica que a classe do objeto que está sendo lido foi vista neste contexto (0x8000).

Ao armazenar objetos, o arquivo mantém um CMapPtrToPtr (o m_pStoreMap) que é um mapeamento de um objeto armazenado para um identificador persistente de 32 bits (PID). Um PID é atribuído a cada objeto exclusivo e a cada nome de classe exclusivo que é salvo no contexto do arquivo. Esses PIDs são distribuídos sequencialmente a partir de 1. Estes PID não têm qualquer significado fora do âmbito do arquivo e, em particular, não devem ser confundidos com números de registo ou outros elementos de identidade.

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

Quando se solicita salvar um objeto num arquivo (geralmente usando o operador de inserção global), deve ser feita uma verificação para assegurar que o ponteiro CObject não seja nulo. Se o ponteiro for NULL, o wNullTag será inserido no fluxo de arquivamento.

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

Se o objeto não tiver sido salvo antes, há duas possibilidades a considerar: ou o objeto e o tipo exato (isto é, classe) do objeto são novos para este contexto de arquivo, 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 CRuntimeClass objeto associado ao objeto que está sendo salvo. Se houver uma correspondência, WriteObject insere uma tag que é bit-wise OR de wOldClassTag e este índice. Se o CRuntimeClass for novo nesse contexto de arquivamento, WriteObject atribuirá um novo PID a essa classe e o insere no arquivo, precedido pelo valor wNewClassTag .

O descritor para esta classe é então inserido no arquivo usando o CRuntimeClass::Store método. CRuntimeClass::Store insere o número do esquema da classe (veja abaixo) e o nome de texto ASCII da classe. Observe que o uso do nome de texto ASCII não garante a exclusividade do arquivo entre aplicativos. Portanto, você deve marcar seus arquivos de dados para evitar corrupção. Após a inserção das informações de classe, o arquivo coloca o objeto no m_pStoreMap e, em seguida, chama o Serialize método 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 armazenamento.

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 , você deve chamar o CArchive método Flush para evitar a corrupção do arquivo.

Observação

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

Carregando objetos do repositório (CArchive::ReadObject)

Carregar (extrair) objetos usa o CArchive::ReadObject método e é o inverso de WriteObject. Assim como WriteObject, ReadObject não é chamado diretamente pelo código do utilizador; o código do utilizador deve chamar o operador de extração seguro para o tipo que chama ReadObject com o CRuntimeClass esperado. Isso garante a integridade do tipo da operação de extração.

Como a WriteObject implementação atribuiu PIDs crescentes, começando com 1 (0 é predefinido como o objeto NULL), a ReadObject implementação pode usar uma matriz para manter o estado do contexto de arquivamento. Quando um PID é lido do armazenamento, se o PID for maior do que o limite superior atual do m_pLoadArray, ReadObject sabe que um novo objeto (ou descrição de classe) se segue.

Números de esquema

O número do esquema, que é atribuído à classe quando o IMPLEMENT_SERIAL método da classe é encontrado, é a "versão" da implementação da classe. O esquema refere-se à implementação da classe, não ao número de vezes que um determinado objeto foi tornado persistente (geralmente referido como a 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 revisa a implementação do método do Serialize objeto permitirá que você escreva código que possa carregar objetos armazenados usando versões mais antigas da implementação.

O CArchive::ReadObject método lançará um CArchiveException quando encontrar um número de esquema no repositório persistente que difere do número do esquema da descrição da classe na memória. Não é fácil recuperar desta exceção.

Você pode usar VERSIONABLE_SCHEMA em conjunto com o operador bitwise OU para combinar com a sua versão do esquema e evitar que essa exceção seja lançada. Ao usar VERSIONABLE_SCHEMA, o seu código pode executar a ação apropriada na função Serialize, verificando o valor de retorno de CArchive::GetObjectSchema.

Chamando Serialização Direta

Em muitos casos, a sobrecarga do esquema de arquivo de objetos gerais de WriteObject e ReadObject não é necessária. Este é o caso comum de serialização dos dados em um CDocument. Neste caso, o método do Serialize é chamado diretamente, não com os operadores de extração ou inserção. O conteúdo do documento pode, por sua vez, usar o esquema de arquivo de objetos mais geral.

Ligar Serialize diretamente tem as seguintes vantagens e desvantagens:

  • Nenhum byte extra é adicionado ao arquivo antes ou depois que o objeto é serializado. Isso não só torna os dados salvos menores, mas permite que você implemente Serialize rotinas que podem lidar com qualquer formato de arquivo.

  • O MFC está ajustado para que as implementações de WriteObject e ReadObject e as coleções relacionadas não sejam vinculadas ao seu aplicativo, a menos que você precise do esquema de arquivamento de objetos mais geral para alguma outra finalidade.

  • Seu código não precisa se recuperar de números de esquema antigos. Isso torna o código de serialização do documento responsável pela codificação de números de esquema, números de versão de formato de arquivo ou quaisquer números de identificação que você use no início dos arquivos de dados.

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

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

Como observado anteriormente, deverá codificar por si mesmo as informações de versão e classe quando chamar Serialize diretamente, o que lhe permitirá ajustar o formato mais tarde, mantendo a compatibilidade com arquivos mais antigos. A CArchive::SerializeClass função pode ser chamada explicitamente antes de serializar diretamente um objeto ou antes de chamar uma classe base.

Ver também

Notas técnicas por número
Notas técnicas por categoria