TN002: formato dati oggetto persistente

Questa nota descrive le routine MFC che supportano oggetti C++ persistenti e il formato dei dati dell'oggetto quando vengono archiviati in un file. Questo vale solo per le classi con le macro DECLARE_edizione Standard RIAL e IMPLEMENT_edizione Standard RIAL.

Problema

L'implementazione MFC per i dati persistenti archivia i dati per molti oggetti in una singola parte contigua di un file. Il metodo dell'oggetto Serialize converte i dati dell'oggetto in un formato binario compatto.

L'implementazione garantisce che tutti i dati vengano salvati nello stesso formato usando la classe CArchive. Usa un CArchive oggetto come traduttore. Questo oggetto persiste dal momento in cui viene creato fino a quando non si chiama CArchive::Close. Questo metodo può essere chiamato in modo esplicito dal programmatore o in modo implicito dal distruttore quando il programma esce dall'ambito che contiene .CArchive

Questa nota descrive l'implementazione dei CArchive membri CArchive::ReadObject e CArchive::WriteObject. Il codice per queste funzioni è disponibile in Arcobj.cpp e l'implementazione principale per CArchive in Arccore.cpp. Il codice utente non chiama ReadObject e WriteObject direttamente. Questi oggetti vengono invece utilizzati dagli operatori di inserimento ed estrazione specifici della classe generati automaticamente dalle macro DECLARE_edizione Standard RIAL e IMPLEMENT_edizione Standard RIAL. Il codice seguente illustra come WriteObject e ReadObject vengono chiamati in modo implicito:

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

Salvataggio di oggetti nell'archivio (CArchive::WriteObject)

Il metodo CArchive::WriteObject scrive i dati di intestazione utilizzati per ricostruire l'oggetto. Questi dati sono costituiti da due parti: il tipo dell'oggetto e lo stato dell'oggetto. Questo metodo è anche responsabile della gestione dell'identità dell'oggetto scritto, in modo che venga salvata solo una singola copia, indipendentemente dal numero di puntatori a tale oggetto (inclusi i puntatori circolari).

Il salvataggio (inserimento) e il ripristino (estrazione) degli oggetti si basa su diverse "costanti manifesto". Questi sono valori archiviati in formato binario e forniscono informazioni importanti all'archivio (si noti che il prefisso "w" indica le quantità a 16 bit):

Tag Descrizione
wNullTag Utilizzato per i puntatori a oggetti NULL (0).
wNewClassTag Indica la descrizione della classe che segue è una novità di questo contesto di archivio (-1).
wOldClassTag Indica che la classe dell'oggetto letto è stata vista in questo contesto (0x8000).

Quando si archiviano oggetti, l'archivio gestisce un CMapPtrToPtr (il m_pStoreMap) che è un mapping da un oggetto archiviato a un identificatore permanente a 32 bit (PID). Un PID viene assegnato a ogni oggetto univoco e a ogni nome di classe univoco salvato nel contesto dell'archivio. Questi PID vengono eseguiti in sequenza a partire da 1. Questi ID PID non hanno alcun significato al di fuori dell'ambito dell'archivio e, in particolare, non devono essere confusi con numeri di record o altri elementi di identità.

CArchive Nella classe i PID sono a 32 bit, ma vengono scritti come 16 bit, a meno che non siano più grandi di 0x7FFE. I PID di grandi dimensioni vengono scritti come 0x7FFF seguiti dal PID a 32 bit. Ciò mantiene la compatibilità con i progetti creati nelle versioni precedenti.

Quando viene effettuata una richiesta per salvare un oggetto in un archivio (in genere usando l'operatore di inserimento globale), viene effettuato un controllo per un puntatore NULL CObject . Se il puntatore è NULL, il valore wNullTag viene inserito nel flusso di archiviazione.

Se il puntatore non è NULL e può essere serializzato (la classe è una DECLARE_SERIAL classe), il codice controlla il m_pStoreMap per verificare se l'oggetto è già stato salvato. In caso affermativo, il codice inserisce il PID a 32 bit associato a tale oggetto nel flusso di archiviazione.

Se l'oggetto non è stato salvato in precedenza, è possibile considerare due possibilità: sia l'oggetto che il tipo esatto (ovvero la classe) dell'oggetto non sono nuovi per questo contesto di archivio oppure l'oggetto è di un tipo esatto già visto. Per determinare se il tipo è stato visualizzato, il codice esegue una query sul m_pStoreMap per un oggetto CRuntimeClass corrispondente all'oggetto CRuntimeClass associato all'oggetto salvato. Se esiste una corrispondenza, WriteObject inserisce un tag bit-wise OR di wOldClassTag e questo indice. Se non CRuntimeClass ha familiarità con questo contesto di archivio, WriteObject assegna un nuovo PID a tale classe e lo inserisce nell'archivio, preceduto dal valore wNewClassTag .

Il descrittore per questa classe viene quindi inserito nell'archivio usando il CRuntimeClass::Store metodo . CRuntimeClass::Store inserisce il numero di schema della classe (vedere di seguito) e il nome del testo ASCII della classe. Si noti che l'uso del nome di testo ASCII non garantisce l'univocità dell'archivio tra le applicazioni. È pertanto consigliabile contrassegnare i file di dati per evitare il danneggiamento. Dopo l'inserimento delle informazioni sulla classe, l'archivio inserisce l'oggetto nella m_pStoreMap e quindi chiama il Serialize metodo per inserire dati specifici della classe. Posizionare l'oggetto nella m_pStoreMap prima di chiamare Serialize impedisce il salvataggio di più copie dell'oggetto nell'archivio.

Quando si torna al chiamante iniziale (in genere la radice della rete degli oggetti), è necessario chiamare CArchive::Close. Se si prevede di eseguire altre operazioni CFile, è necessario chiamare il CArchive metodo Flush per evitare il danneggiamento dell'archivio.

Nota

Questa implementazione impone un limite rigido di indici 0x3FFFFFFE per ogni contesto di archivio. Questo numero rappresenta il numero massimo di oggetti e classi univoci che possono essere salvati in un singolo archivio, ma un singolo file su disco può avere un numero illimitato di contesti di archivio.

Caricamento di oggetti dall'archivio (CArchive::ReadObject)

Il caricamento (estrazione) di oggetti usa il CArchive::ReadObject metodo ed è il contrario di WriteObject. Come per WriteObject, ReadObject non viene chiamato direttamente dal codice utente. Il codice utente deve chiamare l'operatore di estrazione indipendente dai tipi che chiama ReadObject con l'oggetto previsto CRuntimeClass. In questo modo viene garantita l'integrità del tipo dell'operazione di estrazione.

Poiché l'implementazione WriteObject ha assegnato id PID crescenti, a partire da 1 (0 è predefinito come oggetto NULL), l'implementazione ReadObject può usare una matrice per mantenere lo stato del contesto di archivio. Quando un PID viene letto dall'archivio, se il PID è maggiore del limite superiore corrente del m_pLoadArray, ReadObject sa che segue un nuovo oggetto (o una descrizione della classe).

Numeri di schema

Il numero di schema, assegnato alla classe quando viene rilevato il IMPLEMENT_SERIAL metodo della classe, è la "versione" dell'implementazione della classe. Lo schema fa riferimento all'implementazione della classe , non al numero di volte in cui un determinato oggetto è stato reso persistente (in genere definito versione dell'oggetto).

Se si prevede di mantenere diverse implementazioni della stessa classe nel tempo, incrementando lo schema man mano che si modifica l'implementazione del metodo dell'oggetto Serialize , sarà possibile scrivere codice in grado di caricare oggetti archiviati usando versioni precedenti dell'implementazione.

Il CArchive::ReadObject metodo genererà un'eccezione CArchiveException quando rileva un numero di schema nell'archivio permanente diverso dal numero di schema della descrizione della classe in memoria. Non è facile recuperare da questa eccezione.

È possibile usare VERSIONABLE_SCHEMA in combinazione con (OR bit per bit) la versione dello schema per evitare che venga generata questa eccezione. Usando VERSIONABLE_SCHEMA, il codice può eseguire l'azione appropriata nella funzione Serialize controllando il valore restituito da CArchive::GetObjectSchema.

Chiamata diretta della serializzazione

In molti casi il sovraccarico dello schema generale di archivio oggetti di WriteObject e ReadObject non è necessario. Questo è il caso comune di serializzare i dati in un CDocument. In questo caso, il Serialize metodo dell'oggetto CDocument viene chiamato direttamente, non con gli operatori di estrazione o inserimento. Il contenuto del documento può a sua volta utilizzare lo schema di archivio oggetti più generale.

La chiamata Serialize diretta presenta i vantaggi e gli svantaggi seguenti:

  • Nessun byte aggiuntivo viene aggiunto all'archivio prima o dopo la serializzazione dell'oggetto. Ciò non solo rende i dati salvati più piccoli, ma consente di implementare Serialize routine in grado di gestire qualsiasi formato di file.

  • MFC è ottimizzato in modo che le WriteObject implementazioni e ReadObject e le raccolte correlate non vengano collegate all'applicazione, a meno che non sia necessario lo schema di archivio oggetti più generale per altri scopi.

  • Il codice non deve essere ripristinato dai numeri di schema precedenti. In questo modo, il codice di serializzazione dei documenti è responsabile della codifica dei numeri di schema, dei numeri di versione del formato di file o dei numeri di identificazione usati all'inizio dei file di dati.

  • Qualsiasi oggetto serializzato con una chiamata diretta a Serialize non deve usare CArchive::GetObjectSchema o deve gestire un valore restituito (UINT)-1 che indica che la versione è sconosciuta.

Poiché Serialize viene chiamato direttamente nel documento, non è in genere possibile che i sotto-oggetti del documento archiviino i riferimenti al documento padre. Questi oggetti devono essere assegnati in modo esplicito a un puntatore al documento contenitore oppure è necessario usare la funzione CArchive::MapObject per eseguire il mapping del CDocument puntatore a un PID prima dell'archiviazione di questi back pointer.

Come indicato in precedenza, è consigliabile codificare manualmente le informazioni sulla versione e sulla classe quando si chiama Serialize direttamente, consentendo di modificare il formato in un secondo momento mantenendo comunque la compatibilità con le versioni precedenti con i file meno recenti. La CArchive::SerializeClass funzione può essere chiamata in modo esplicito prima di serializzare direttamente un oggetto o prima di chiamare una classe base.

Vedi anche

Note tecniche per numero
Note tecniche per categoria