Paolo Guccini

"Impossibile" non è mai la risposta giusta

GESTIRE FILE
CON RECORD A LUNGHEZZA CONSTANTE

I sorgenti sono disponibili per il download.

E' possibile scrivere un programma che possa indifferentemente operare su file di tipo testo con record a lunghezza variabile o costante senza dover ricorrere ad una infinita serie di if. . . else? Ebbene sì!


Nei due articoli precedenti abbiamo visto due classi di nome RecMgr e RecCLenMgr che rispettivamente facilitavano la gestione dei record a lunghezza variabile o fissa. Ora concluderemo vedendo la classe AllRecMgr che rappresenta per il programma un’interfaccia unica per la gestione di entrambi i tipi di record.

I principi di AllRecMgr

AllRecMgr è base class di RecMgr e RecCLenMgr. In essa sono contenute varie funzioni che vedremo in seguito. Ora concentriamoci sui motivi tecnici di questo tipo di organizzazione fra le varie classi.
Lo scopo originario era disporre di un sistema per poter scrivere programmi in grado di gestire tipi di file diversi senza ricorrere ogni volta ad una serie sterminata di if...else oppure switch.
I linguaggi Object Oriented offrivano le caratteristiche necessarie: il run-time polimorphism.
Per chi ancora non lo ha affrontato, lo si potrebbe spiegare in estrema sintesi dicendo che si tratta di un sistema di gestione delle classi e relative funzioni mediante puntatori che, disponendo di un puntatore a una base class inizializzato con l’indirizzo di una derived class, consente di richiamare una funzione membro della derived class a cui punta.
Di conseguenza, modificando il puntatore affinché punti ad un’altra derived class (logicamente derivata sempre dalla stessa base class), richiamando la stessa funzione (dichiarata e disponibile in entrambe le derived class) verrà eseguita quella appartenente a quest’ultima classe. Se avete qualche perpiessità non preoccupatevi, entro la fine di quest’articolo avrete le idee più chiare.
Come nei precedenti articoli, abbrevieremo i file con record a lunghezza variabile con VRL e quelli a lunghezza costante con CRL.
Vediamo adesso una porzione di un programma che sfrutta queste classi per copiare un file su di un altro. In particolare, il programma deve essere in grado di poter leggere un file di tipo CRL oppure VRL e scriverne un altro anch’esso di tipo CRL o VRL; per entrambi i file i tipi vengono definiti a run-time.
Il programma dovrà prevedere vari passi sia per il file di input che quello di output: innanzi tutto allocare un AllRecMgr * (puntatore alla base class AllRecMgr).
Poi, allocherà la classe opportuna per la gestione dei record (RecMgr oppure RecCLenMgr).
Infine l’indirizzo della classe creata verrà copiato in AllRecMgr *.
A questo punto è possibile richiamare AllRecMgr::readRecord() per ottenere la lettura di un record indipendentemente dalla struttura logica del file (record a lunghezza fissa o variabile) in quanto la funzione readRecord() che verrà eseguita dipenderà dal tipo di classe (derived) a cui AllRecMgr punta.

Funzioni membro

Prima di passare in rassegna tutte le funzioni membro di questa classe, vi segnalo una modifica apportata alla classe rispetto alle versioni pubblicate in precedenza: alcune funzioni sono state spostate da public a protected o private in osservanza della filosofia delle classi.
Passiamo ora in rassegna le varie funzioni che sono state raccolte per gruppi omogenei per facilitarne l’analisi.

Funzioni già viste

Le funzioni readRecord(), splitting() e merging() sono già state trattate negli articoli relativi alle derived class apparsi i mesi scorsi. Unica osservazione è che sono tutte funzioni di tipo pure virtual; questa scelta è stata fatta considerando che solo questo tipo di funzioni permetteva di fare la sola dichiarazione di funzione senza scriverne anche il relativo codice sorgente (completamente inutile a livello di AllRecMgr). Non ci dilungheremo oltre su queste funzioni.
Abbiamo già visto le funzioni clearFld(), getFiller(), getFileEnd(), getRecEnd() e le relative set...(), perciò anche per esse non ci dilungheremo.

Funzioni di stato ed errori

La funzione getErrorType() permette al programma di conoscere se la classe ha riscontrato degli errori. Ad ogni chiamata, questa funzione restituisce l’eventuale codice d’errore.
Nel caso accadano più errori in successione, questa funzione restituisce sempre e solo il primo.
La scelta di questo metodo è semplice: spesso accade che una situazione di errore causi a cascata altri errori. Se venisse riportato l’ultimo errore sarebbe più complesso determinare ove è nato il problema.

  • getErrorField() permette di sapere su quale campo è avvenuto l’errore.
    Questa funzione viene utilizzata in abbinamento a getErrorType() per ottenere maggiori informazioni sull’ anomalia.
  • resetError() permette di azzerare le flag di errore restituiti dalle precedenti funzioni.
    Si dovrebbe implementare un controllo di errori per ogni file alla fine di ogni ciclo; nel caso si siano verificati degli errori gestibili senza dover interrompere il programma si deve richiamare resetError() affinché alla successiva chiamata di getErrorType() e getErrorField() non restituiscano i vecchi codici di errori. In altre parole, i codici di errori possono venire azzerati solo esplicitamente tramite questa funzione.
  • setErrorOnField() è la funzione utilizzata dalle classi per memorizzare i codici di errore.
    Sarebbe stato forse opportuno renderla inacces sibile al programma in rispetto della logica OOP. Ritengo che non dovrebbe mai essere utilizzata. Discorso simile anche per la funzione init(), la quale si occupa di eseguire le inizializzazioni di variabili e flag.
  • getFileStatus() è una funzione importante; è essa infatti che il programma richiama per conoscere se il file ha raggiunto la condizione di End Of File.
    Sebbene appaia ovvio e più sempli ce richiamare la funzione ifstream::eof(), nella pratica non è possibile: i file VRL hanno un carattere che dichiara la fine del file (di solito il carattere EOF, hex OxlA), conseguentemente la fine logica del file precede quella fisica e quindi ifstream::eof() non è in grado di restituire il valore "vero" al momento opportuno.

Funzioni per il tipo di campi

I campi di un record possono essere di diverso tipo; essi si suddividono principalmente in alfanumerico e non alfanumerico. Quelli non alfanumerici possono essere: numerici in formato binario a 2,4 e 8 byte, oppure altri ancora.
Di conseguenza le funzioni merging() e splitting() devono sapere le caratteristiche dei campi su cui operano. A tal scopo questa classe memorizza all’in terno di un array di char di nome MtxTipoCampi la tipologia di ogni singolo campo.
L’accesso a quest’array avviene tramite funzioni specifiche. La funzione setFldType(), consente di memorizzare le informazioni all’interno dell’array. Essa è overloaded, ovvero può essere richiamata in diversi modi: o passandole un char per il tipo di campo e un int per identificare a quale campo ci si sta riferendo, oppure con una stringa contenente la descrizione del tipo di tutti i campi e la sua lunghezza in byte. Inoltre è disponibile anche setFldlype_Coded(), essa accetta come argomento un char * come setFldType ma le ripetizioni successive del tipo di campo possono essere abbreviate anteponendo al tipo il numero di campi contigui identici.
Ad esempio, se abbiamo un record composto da 3 campi alfanumerici, uno numerico, due alfanumerici, potremmo utilizzare la stringa "3AN2A" come argomento di quest’ultima funzione.
In realtà questa funzione nasce dall’esigenza di fornire all’utente un’interfaccia il più semplice e comoda possibile: immaginatevi un record composto da 20 campi alfanumerici e 10 numerici e dover quindi digitare una stringa di 30 caratteri ed invece poter scrivere: "20C10N".
Comunque, al momento, il tipo di campo è attivo solo per i file VRL, e riconosce due possibilità: ‘C’ per campo alfanumerico e ‘N’ per campo non alfanumerico. Se volete implementare altri tipi di campi è sufficiente intervenire su merging() e splitting() inserendo apposite routine che dovranno poi essere attivate automaticamente in base al tipo campo trovato in MtxTipoCampi.

  • getFldType() restituisce il tipo dell’n-esimo campo. È il complemento alle funzioni precedenti. Invece getFldTypeTotal() restituisce il totale dei tipi di campi dichiarati; nell’esempio "2OC1ON" restituirebbe 30.
  • sizeFldType() si occupa di dimensionare l’array contenente i tipi dei campi. È necessario richia marla prima di eseguire la setFldType(). Nel caso fosse necessario ingrandire o ridurre l’array, si può chiamare la funzione resizeFldType(); questa funzione può anche essere utilizzata al posto di sizeFldType().

Funzioni per i campi trovati

Può accadere che il programma non fornisca alla classe AllRecMgr nessuna informazione sul tipo dei singoli campi; evidentemente, questo è plausibile solo nel caso di file VRL.
In questo caso, la sequenza splitting() e merging() non genererebbe due record uguali in quanto merging() non saprebbe quando delimitare un campo o meno.
Per evitare all’utente del programma di dover specificare ogni volta le caratteristiche di ogni campo, AllRecMgr fornisce un’agevolazione: ad ogni chiamata di splitting(), viene memorizzato il tipo di ogni campo letto: delimitato o non delimitato: Quindi è possibile eseguire un ioop che copia il tipo di campo trovato sul record (funzione getFldTypeFound() ) sull’array dei tipi campi (tramite la funzione setFldType() ) che viene sfruttato da merging(). Non preoccupatevi se non vi è chiaro: lo approfondiremo il mese prossimo.

In pratica...

Vediamo ora qualche esempio pratico di come operare attraverso questa classe.

Nel primo listato troviamo un breve programma che mostra come gestire le classi RecMgr e AllRecMgr attraverso un puntatore di tipo AllRecMgr. Inizia con main() che definisce un puntatore di nome arm e richiama una funzione di nome createMgr() (questi nomi sono arbitrari e non sono membri delle classi); il suo compito consiste nell’allocare una classe in relazione a quanto richiestole mediante il parametro tipo e memorizzarne l’indirizzo in arm, al quale può accedere in quanto ne ha ricevuto l’indirizzo come secondo argomento.
Il parametro tipo viene prelevato dalla linea di comando (argv per meglio evidenziare che il programma può non conoscere fino al momento dell’esecuzione su quale tipo di file opererà; argv può essere ‘V’ per VRL oppure ‘C’ per CRL.
L’ultima istruzione all’interno di main() richiama una funzione diagnostica (che è utilizzabile solo se tutti i moduli vengono compilati definendo globalmente il simbolo DBG9) di nome DBGcall(). Essa non fa altro che visualizzare il nome della classe a cui appartiene. In pratica se arm punta a una classe RecMgr, DBGcall() visualizzerà "RecMgr", mentre se punta ad una classe RecCLenMgr visualizzerà "RecCLenMgr".
Ora che abbiamo visto in pratica come opera e viene applicato il polimorfismo applicato alla DBGcall(), siamo pronti per affrontare un programma completo e funzionante: il listato 2. La sua struttura è analoga a quella del primo listato, ma sono state aggiunte alcune variabili:

  • recBuffer: buffer in cui viene caricato il record letto da readRecord();
  • recLen: variabile in cui readRecord() memorizza la lunghezza del record letto;
  • fieldTot: variabile in cui splitting() memorizza il numero dei campi estratti dal record;
  • fieldVaiue: array che conterrà gli indirizzi dei campi che verranno estratti dal record attraverso la funzione splittingO.

Vi sono altre variabili che acquisiscono differenti funzioni in relazione alla classe su cui operano: se sono utilizzate con RecCLenMgr:

  • fieldLen: array contenente le lunghezze di ogni singolo campo. Viene impiegato principalmente dalla funzione splitting();
  • fieldPrevisti: numero dei campi che il record contiene. Il suo valore opera anche come limite massimo per gli array;

Se invece vengono utilizzate per una classe RecMgr:

  • fieldLen: dopo la splitting() contiene la lunghezza dei singoli campi estratti;
  • fieldPrevisti: numero dei campi trovati nel record;
  • recLenPrevista: lunghezza del record letto median te readRecord().

Il programma, come nell’esempio precedente, determina il tipo di file da leggere basandosi su quanto specificato sulla linea di comando al momento del lancio del programma: la ‘V’ o la ‘C’ definiscono il tipo di file ed anche il nome.
Dopo l’apertura del file e l’allocazione del gestore record troviamo una if. Essa serve ad inizializzare le variabili con i dati relativi al tipo di file che si è deciso di leggere.
Segue il loop del programma, il quale avrà termine quando il file raggiungerà la condizione di EOF. Lo scopo del loop è la lettura di un record seguita dalla estrazione dei campi dal record. Un ciclo for visualizza i vari campi estratti.
Come sempre una clearFld() termina il ciclo.
Il terzo ed ultimo listato, decisamente più corposo dei precedenti, rappresenta un valido prototipo di programma che gestisca un file in input ed un altro in output, senza conoscere a priori i tipi dei due file. Infatti, come nei precedenti esempi, sarà l’utente a determinare i tipi dei due file attraverso la riga di comando, ma questa volta saranno necessari due argomenti, il primo per il file di input ed il secondo per il file di output.
Il programma contiene al suo interno tutte le caratteristiche dei file, quali i nomi e le dimensioni. Questo vi permetterà di eseguire tutte le prove che ritenete utili per comprendere bene come operano le classi e le funzioni. Evidentemente, per costruire un program ma vero e proprio, sarà necessario sostituire i dati inseriti all’interno con funzioni che consentano all’utente di specificare le caratteristiche dei file di volta in volta secondo le proprie necessità. Sebbene questo li sia il più importante, non lo descriverò dettagliatamente come per i precedenti in quanto ho preferito inserire molti commenti all’interno del sorgente affinché sia autoesplicante: questo vi permetterà di poterlo guardare in futuro senza avere la necessità di tenere la rivista sempre aperta (tenetela comunque a portata di mano...).
Tenendo come base il secondo ed il terzo listato per sviluppare i programmi, ritengo non dovreste incontrare difficoltà di sorta. Ricordatevi comunque che è sempre buona norma eseguire dei controlli affinché eventuali errori possano essere individuati e segnalati dal programma all’utente: le funzioni come la getErrorType() sono state implementate proprio per questo scopo.
Non mi stancherò mai di ripetere di evitare il più possibile i messaggi sibillini quali "Errore interno" oppure "Errore 21" in quanto non aiutano assolutamente a comprendere cosa è accaduto e spesso risultano irritanti; invece sarebbe assai indicato prevedere una funzione specifica per la visualizzazione dei messaggi d’errore, la quale riceva il codice di errore e provveda a far apparire una chiara descrizione del problema occorso ed eventuali informazioni aggiuntive, come il numero di record oppure il numero di campo.

Conclusioni

Questo mese abbiamo visto come opera la classe AllRecMgr e, attraverso lo studio dei principi dei quali si avvale, abbiamo dato un’occhiata anche al concetto di polimorfismo. Ora disponete di tutte e tre le classi, sia come sorgenti che spiegazioni. Non vi dovrebbe essere quindi difficile estendere queste classi in base alle vostre specifiche necessità. Buon lavoro!

Il testo e' stato acquisito tramite OCR dalla rivista su cui e' stato pubblicato e velocemente ricontrollato.
Le segnalazioni di errori saranno molto gradite e si possono fare alla paginaContatti.

Tratto da:
Paolo Guccini
Rivista DEV Computer Programming
Edizioni Infomedia
Aprile 1996