A. Veneziani - Files di testo in C#

Pagina creata da Stefania Tosi
 
CONTINUA A LEGGERE
A. Veneziani – Files di testo in C#
Utilità dei files di testo
Come praticamente tutti i linguaggi di programmazione anche C# permette di operare con i file. Come
dovrebbe essere noto i files servono alle applicazioni per salvare i dati di interesse oltre il periodo stesso della
pura esecuzione. La gestione dei files è quindi determinante per salvare dati che altrimenti verrebbero persi
alla chiusura dell’applicazione stessa. Tali dati quindi possono, grazie ai files perdurare per un tempo lungo
a piacere o successivamente essere modificati.
Esistono tipi diversi di file.
       Files di testo – contenenti solo testo, avente diversi significati
       File binari – ad esempio i files eseguibili, che contengono codice macchina e quindi il cui significato è
          per eccellenza binario
       Files di record – files il cui contenuto è suddiviso in campi strutturati di lunghezza fissa.
Si ricorda che i file di testo (file con codifica in formato testo) sono molto comuni nei sistemi di calcolo. Ad
esempio sono file di testo: file sorgenti di programmi scritti in qualunque linguaggio, file .ini (di
configurazione) del sistema Windows o di sue applicazioni, file .html, ossia pagine Web, o le stesse quando
contengano codice di vari linguaggi per il Web (Javascript, PHP, ASP, ecc…), file XML, e molti altri casi.
Noi andremo ad operare solo con i files di testo, i quali risolvono e permettono di affrontare comunque una
vasta gamma di problemi.
Le fasi fondamentali per svolgere operazioni sui files sono:
      a) Apertura del file – indicando la sua posizione e nome sul disco e indicando il tipo di operazioni che
          si intendono effettuare su essi.
      b) Lettura / scrittura dei dati
      c) Chiusura del file
Durante le operazioni di lettura il punto di lettura è memorizzato da un apposito indice che avanza in
automatico via via che i dati vengono letti dal file.
Viceversa la scrittura nei file di testo avviene, di norma, alla fine del file.
Un file di testo può essere facilmente riconosciuto, al di là dei suoi contenuti specifici o dell’estensione del
file, per il fatto che il suo contenuto può essere letto aprendo il file con un programma detto editor di testo.
Tali programmi sono del tutto comuni, e molti di essi sono software open, quindi reperibili liberamente, senza
pagamento. Il più comune e più noto rappresentate della categoria degli editor di testo è il programma
Blocco nome (Notepad), presente di default in tutte le versioni di Windows. Ovviamente esistono editor di
testi più evoluti di Blocco note, che hanno funzionalità più complesse e complete, quali:
       Riconoscimento del linguaggio contenuto nel testo e relativa colorazione del codice
       Potenti funzioni di undo e redo del testo scritto
       Multiediting in un unico programma (più editor disponibili in conteporanea)
       Split della finestra di editor ed editazione di due parti di codice diverse e distanti
       Gestione di file vari formati ASCII, Unicode, altri

Apertura di un file di testo
Per operare su un file la prima operazione che deve essere fatta preliminarmente a tutte le altre è l’apertura
del file. Prima di operare su di esso il file deve essere aperto. In tal caso esistono due possibilità:
     Il file è esistente
     Il file non esiste, ma l’apertura avviene con permessi che consentono la creazione del file
Nel secondo caso il file che non esiste ancora viene creato con il nome indicato nelle operazioni di apertura.
Esiste un terzo caso, ossia se l’operazione di apertura non è previsto crei files, in tale evenienza un errore
verrà ritornato senza creare il file.

                                                     Pagina 1
Nel caso dell’ utilizzo di StreamWriter questo terzo caso non si verifica in quanto l’utilizzo di questa classe
prevede sempre la creazione del file nel caso esso non sia esistente.
In C# vi sono diversi modi per effettuare operazioni simili sui files di testo.
Indichiamo qui sotto quali noi utilizzeremo:
       Operazioni di creazione e scrittura di un file tramite la classe StreamWriter
       Operazioni di lettura di un file tramite la classe StreamReader
I file di testo possono essere aperti in scrittura in due modalità:
       In riscrittura (troncamento1)– il contenuto del file viene azzerato e riscritto ogni volta che il file viene
           aperto
       In aggiunta (append) – al contenuto del file vengono aggiunti altri dati in coda.

Scrittura di un file di testo
In certi casi si desidera aprire il file per scrivere in esso. In questo caso una possibile metodica C#2 è quella di
aprire il file utilizzando un oggetto della classe StreamWriter. La classe StreamWriter è utile per effettuare
scritture ogniqualvolta si abbia uno “stream” (canale) di comunicazione e si debba scrivere su di esso.
In questo caso il “canale di comunicazione” è rappresentato dal flusso di dati che si desiderano scrivere sul
file, una volta che sia stato aperto.
L’istruzione che permette l’apertura di un file di testo ha la forma:

         StreamWriter  = new StreamWriter(, );

se il file esiste ed il flag append è true l’operazione viene vista come un aggiunta di dati in coda in un file già
esistente. Altrimenti l’apertura opera in scrittura da zero (troncamento).
Il path deve indicare il percorso completo o relativo del file e deve essere completo ovviamente del nome del
file sul quale si desidera operare. Si ricorda che l’estensione3 generica (opportuna) per un file un file di testo
è .txt.
Nel caso in cui il file non esista, esso viene di default creato tramite tale operazione.
Per utilizzare gli oggetti delle classi StreamWriter e StremReader deve essere incluso (using) il package
System.IO.
Successivamente è possibile utilizzare la variabile oggetto del tipo StreamWriter per effettuare scritture nel
file:
           StreamWriter sw = new StreamWriter(“.\\pippo.txt”);
           ….
           sw.WriteLine(….);
           ….
Si noti che il path (percorso) può essere espresso in vari modi:
            Con il solo nome del file – in questo caso il file deve essere presente o viene creato nella directory
               ove risiede il file eseguibile (ad. esempio “pippo.txt”)
            Con un path relativo – il path relativo si riferisce alla directory dove si trova il file .exe della
               applicazione che stà utilizzando il file. Ammettiamo che il file sia stato sistemato in una
               sottodirectory files delle directory bin\debug dove è presente l’ .exe, allora il path potrà essere
               espresso con un percorso relativo da quella directory: (ad es. “.\\files\\pippo.txt”)

1 In inglese truncate
2 Ripetiamo che esistono molteplici tecniche ed istruzioni di diverso tipo per operare con i file di testo in C#; quella presentata è
una sola di quelle possibili.
3 Si ricorda anche che l’estensione di un file è rappresentata di norma, nei sistemi Windows/DOS, da tre caratteri in coda al nome

vero e proprio preceduti da un punto. Ad esempio pippo.txt (file con estensione .txt)
                                                              Pagina 2
    Infine il terzo ed ultimo caso di percorso, il percorso indicato in modo assoluto. In esso si prende
             come riferimento la root dell’unità su cui si sta operando, quindi un path assoluto potrebbe
             essere: C:\\Works\\C#\\console\\files\\leggi_e_stampa\\bin\\Debug\\files\\pippo.txt
Si osservi anche una regola importante. Il simbolo di backslash ( \ ), che server per separare il nome delle
singole directory e che in DOS/Windows è singolo, qui deve essere sempre raddoppiato. Questo in quanto
in numerosi linguaggi (tra cui C#, ma anche C e C++ ed altri), lo stesso simbolo è preposto a caratteri detti
speciali quali:

                          Carattere speciale               Rappresentazione
                          Ritorno carrello                 \r
                          Passa a riga successiva          \n
                          Tabulazione                      \t
E numerosi altri.
In pratica il backslash in tali linguaggi usano il carattere di backslash per indicare una sequenza di due caratteri
che indica la presenza di un carattere speciale. Dato tutto ciò per rappresentare lo stesso backslash si deve
raddoppiare il backslash stesso \\.

Le regole e gli effetti del metodo WriteLine applicate ad un oggetto StreamWriter sono analoghe a quelle
dell’analogo metodo applicato alla classe Console (Console.WriteLine(…)). Ovviamente è possibile scrivere
qualunque dato numerico, alfanumerico o booleano su un file di testo.
La scrittura su un file procede anch’essa in modo analogo a quanto avviene solitamente su console. Di norma
non si procede a ritroso, ma la scrittura procede progressivamente in avanti. Via via si possono inserire
ritorni a capo opportuni, che producono una nuova linea nel file di testo.
In pratica il testo scritto con la stessa sequenza di operazioni e di comandi di a capo su console o su file,
produce gli stessi effetti su entrambi gli elementi di gli output.

Chiusura di un file di testo in scrittura
La classe StreamWriter prevede il comando per effettuare una opportuna operazione di chiusura delle
operazioni di scrittura precedentemente attivate (con l’apertura) su un file:
         ….
         sw.Close();
         …
Il metodo non ha parametri in quanto tutte le informazioni sullo stream da chiudere sono già presenti nella
variabile oggetto sw.
Sebbene non sia tassativa la chiusura di un file (che viene effettuata automaticamente dal programma), è
opportuno effettuarla in quanto essa effettua di default lo svuotamento del buffer di scrittura su disco e
quindi forza il sistema a riportare tutti i contenuti del buffer nel file. Fino a quando questo non succede, la
scrittura nel file potrebbe risultare incompleta, in quanto i contenuti scritti nel buffer potrebbero non essere
stati effettivamente scritti nel file di testo.

La lettura di un file di testo
Le operazioni di lettura su un file di testo possono essere effettuate tramite l’utilizzo della classe
StreamReader che ha funzionalità simmetriche a quelle della classe StreamWriter precedentemente
considerata. L’apertura in lettura di un file si effettua dunque:

        StreamReader  = new StreamReader();

La composizione del path segue le stesse regole che sono state elencate sopra in dettaglio per l’apertura in
scrittura di un file. In questo caso si possono avere due evenienze:
                                                   Pagina 3
    Il file di testo esiste -> esso viene aperto e letto
             Il file di testo non esiste -> viene prodotto un errore (detta in C# eccezione) del tipo
              FileNotFoundException
Successivamente, esistono appositi metodi anche in questo caso per leggere dal file il suo contenuto.
Anche in questo caso i metodi considerati sono analoghi a quelli utilizzati sulla classe Console.
In questo caso però esistono ulteriori problematiche. Nel file esiste una quantità di dati non quantificabile
a priori, e spesso notevole (un file di testo può essere costituito da migliaia di righe da leggere.
Ciò comporta che sia esclusa una lettura comandata da singole istruzioni riga per riga, ma si debba instaurare
un cosiddetto ciclo di lettura dei dati. Tale ciclo in questo caso dovrà individuare quando i dati da leggere
siano esauriti.
La lettura avviene tenendo memoria del punto di lettura precedentemente raggiunto4, per proseguire con il
successivo passo di lettura. Quando tale punto di lettura raggiunge la fine del file è evidente che non vi sono
più dati da leggere.
Un ciclo di lettura avrà quindi la seguente conformazione-tipo:
        StreamReader sr;
        string linea;
         ….
        sr = new StreamReader("dati.txt");
        ….
        while (! sr.EndOfStream)
        {
            linea = sr.ReadLine();
            Console.WriteLine(linea);
        }
Questo per un esempio di semplice lettura del testo riga per riga con stampa diretta sulla console dello stesso.
In pratica questo ciclo, dato un file di testo riporta direttamente e fedelmente il contenuto del file sulla
console, qualunque sia il suo contenuto.
Ovviamente le operazioni legate alla lettura del file sono le più svariate e possono essere diversa da questa.
Si noti che nella condizione di ciclo c’è un controllo che verifica se il punto di lettura sia o no arrivato alla fine
del file, leggendo una proprietà (EndOfStream) che di volta in volta rende il valore (false – il punto di lettura
non è alla fine del file o true – il punto di lettura è alla fine del file).

Riposizionamento nel file in lettura
Come comprensibile, durante la lettura è possibile che il file debba essere letto una seconda volta per
ulteriori elaborazioni sul suo contenuto. L’operazione di riposizionare il cursore di lettura è semplice e
possibile. Basta utilizzare l’istruzione:
         …
         sr.BaseStream.Position = 0;
         …
Nell’esempio indicato il riposizionamento del cursore avviene riportando lo stesso all’inizio del file di testo
(scostamento, rispetto all’origine di 0 caratteri). Questo è il caso senz’altro più frequente di utilizzo di questa
istruzione. In altri casi può capitare di desiderare spostare il cursore in qualche altra posizione intermedia.
Si ricordi a questo proposito che in un file di testo su più righe, ogni fine riga conta quanto 2 caratteri, essendo
formato infatti dall’accoppiamento dei codici 10 e 13 ASCII. Quindi anche se i fine linea non appaiono visibili
vanno considerati nei calcoli per il riposizionamento opportuno del cursore di lettura.

Chiusura del file di testo in lettura
Esiste per simmetria, come per lo stream in scrittura (StreamWriter), anche nello stream in lettura, una
operazione di chiusura dello stream:

4
  Si potrebbe paragonare al dito che le maestre insegnano a tenere, durante la lettura di un testo, sulla parola che leggevamo, alle
elementari.
                                                             Pagina 4
….
        sr.Close();
        ….
Che ovviamente và eseguita al termine delle operazioni. Il comando comunque può essere omesso, in
quanto il programma effettua una chiusura automatica dello stream stesso, senza quindi comunque dare
errore se lo stream in lettura non viene esplicitamente chiuso.

Altri tipi di letture di un file
Lettura di un file di testo per singoli caratteri
Mostriamo qui una variante della comune lettura per righe, vale a dire una lettura del file di testo che proceda
un carattere alla volta. Talora si rivela più pratica e può essere utilizzata in alcuni specifici problemi:
        StreamReader sr;
        char ch;
        …
        sr.BaseStream.Position = 0;
        while (! sr.EndOfStream)
        {
            ch = (char) sr.Read();
            Console.Write(ch);
        }
        …
Come si nota lo schema di base dell’operazione di lettura è lo stesso, ma invece di andare a leggere un
intera linea di testo (precedentemente assegnata ad una stringa), si legge un solo carattere, assegnandolo
ad un char. Il metodo applicato ad sr in questo caso è il Read() senza parametri. Esso rende il codice
Unicode del carattere letto e lo rende come intero (trattandosi appunto di un codice). Si rende quindi
necessaria una operazione di cast fra int e char. Dopodiché volendo è possibile stampare il carattere.
Anche in questo esempio l’output su console del frammento di programma coincide perfettamente con il
contenuto del file (come si potrebbe vedere in un editor di testo, ad esempio), ma ovviamente la tecnica di
lettura dei dati è diversa dalla precedente per righe.

Lettura in un'unica operazione di un file di testo
In taluni casi piuttosto che effettuare un ciclo che legga linea per linea il file si preferisce elaborarlo in modo
posticipato, effettuando la lettura in una sola operazione, ed inserendo tutto quanto letto in una variabile
stringa. In questo caso quindi tutto il contenuto del file diviene il contenuto della var. stringa.
In C# esiste un apposito metodo per effettuare questo tipo di operazione utilizzando oggetti della classe
StreamReader. Ecco un esempio:
         ….
        StreamReader sr;
        string testo;
        …
       sr.BaseStream.Position = 0;

       testo = sr.ReadToEnd();
       Console.Write(testo);
       …
Che ha l’effetto delle altre routine di lettura dei dati sul file, ossia stampa tutto il testo, compresi caratteri
di a capo in esso contenuti.

Lettura di dati “comma delimited”
Supponiamo che nel file siano memorizzati dati separati da opportuni segni di separazione (nel nostro caso
virgole). Supponiamo poi di voler leggere i dati, ovviamente suddivisi in modo opportuno.
Ci troviamo di fronte ad un classico problema di lettura di un file dove i dati sono delimitati da virgole, ossia
“comma delimited”.

                                                     Pagina 5
Spesso questo tipo di file con delimitazioni interne tra i dati è utilizzato per raggruppare gruppi di dati
correlati fra loro. Supponiamo di andare a leggere un file in cui su ogni riga il primo dato sia il cognome, il
secondo sia il voto di Italiano, il terzo il voto di Matematica, il quarto il voto in Informatica.
Questi dati potrebbero essere inseriti in un vettore di variabili strutturate per essere poi elaborati
liberamente. Vediamo come si potrebbe effettuare la lettura degli stessi:
Consideriamo che il file di testo potrà avere un contenuto del tipo:
        verdi,4,7,9
        rossi,5,8,3
        neri,9,5,4
        bianchi,3,7,8
dove i dati sono appunto delimitati fra loro da virgole. Il codice C# atto a caricare in un programma tali dati
è il seguente:
        ...
         struct Alunno
         {
             public string cognome;
             public int voto_ita;
             public int voto_mate;
             public int voto_info;
         }
        ...
         static void Main(string[] args)
         {
             StreamReader sr;
             string linea;
             string[] dati;
             Alunno[] registro = new Alunno[20];
             int i;

              sr = new StreamReader("dati.txt");

              i = 0;
              while (!sr.EndOfStream)
              {
                  linea = sr.ReadLine();
                  dati = linea.Split(',');

                   registro[i].cognome = dati[0];
                   registro[i].voto_ita = int.Parse(dati[1]);
                   registro[i].voto_mate = int.Parse(dati[2]);
                   registro[i].voto_info = int.Parse(dati[3]);
                   i++;
              }
            ...
Questo è il frammento di codice che permette di caricare in un vettore di struct di elementi Alunno (chiamato
registro); i dati vengono:
      per prima cosa letti per righe
      il contenuto delle righe viene suddiviso in più stringhe contenenti i singoli dati grazie alla presenza
         delle virgole
      i dati vengono opportunamente convertiti ed assegnati ai campi del vettore strutturato.
In questo modo possono essere elaborati liberamente nel programma.

Altre operazioni sui file
In alcuni casi effettuare operazioni sui file comportano anche operazioni sul file system . Si deve specificare
che le operazioni considerate qui di seguito non riguardano solo i file di testo, ma tutti i tipi di files
indistintamente.
Le più comuni tra esse sono:
      Controllo dell’esistenza di un certo file in un certo path
                                                   Pagina 6
   Cancellazione di un file
       Ridenominazione di un file.
       Spostamento di un file
       Copia di un file

Controllo di esistenza di un file
E’ possibile effettuarlo utilizzando la classe File. La classe in questo caso viene usata senza allocare variabili
oggetto, ma direttamente. Il metodo (statico) che essa usa per il controllo dell’esistenza del file è Exists(….),
applicato al path + nome del file di cui si desidera controllare la presenza:
         …
         File.Exists();
         …
Il metodo rende un booleano che indica se il file è presente (true) o meno (false). Usualmente tale istruzione
è utilizzata in istruzioni di selezione.

Cancellazione di un file
In questo caso ci si pone come scopo quello di rimuovere un file dal file system. In questo caso deve essere
utilizzato il metodo (statico) Delete:
         ….
         File.Delete();
         ….
Nulla accade se il file non esiste.

Ridenominazione di un file o suo spostamento
Il problema di muovere un file viene risolto tramite il metodo Move, sempre appartenente alla classe File.
Esso prevede due parametri, che prevedono l’input del path + nome del file originario (primo parametro) e
path + nome del path di destinazione (secondo parametro):
         ….
         File.Move(, );
         ….
Ovviamente questa operazione può servire ed è pensata anche nel caso si voglia rinominare il file. Il path di
origine in questo caso potrà essere il medesimo.
L’operazione produrrà un eccezione FileNotFoundException se il file di origine non esistesse.
L’operazione produrrà una eccezione IOException se il file di destinazione dovesse già esistere e dovrebbe
quindi essere sovrapposto.

Copia di un file
Sempre utilizzando la classe File, si può operare tramite il metodo Copy, che aspetta sempre due parametri,
uno indicante il path ed il nome del file originario, ed il secondo il path ed il nome del file sorgente.
        ….
        File.Copy(, );
        …
Il comando genera una copia del file di origine, eventualmente con un nome diverso.
L’operazione produrrà un eccezione FileNotFoundException se il file di origine non esistesse.
L’operazione produrrà una eccezione IOException se il file di destinazione dovesse già esistere e dovrebbe
quindi essere sovrapposto.

                                                    Pagina 7
Puoi anche leggere