Questo sito utilizza cookies solo per scopi di autenticazione sul sito e nient'altro. Nessuna informazione personale viene tracciata. Leggi l'informativa sui cookies.
Username: Password: oppure
Guida al Visual Basic .NET - Input e Output su file

Guida al Visual Basic .NET

Capitolo 56° - Input e Output su file

<< Precedente Prossimo >>

 

Gli Stream

Le operazioni di input e output, in .NET come in molti altri linguaggi, hanno come target uno stream, ossia un flusso di dati. In .NET, tale flusso viene rappresentato da una classe astratta, System.IO.Stream, che espone alcuni metodi per accedere e manipolare i dati ivi contenuti. Dato che si tratta di una classe astratta, non possiamo utilizzarla direttamente, poiché, appunto, rappresenta un concetto astratto non istanziabile. Come già spiegato nel capitolo relativo, classi del genere rappresentano un archetipo per diverse altre classi derivate. Infatti, un flusso di dati può essere tante cose, e provenire da molti posti diversi:

  • può trattarsi di un file, come vedremo fra poco; allora la classe derivata opportuna sarà FileStream;
  • può trattarsi di dati grezzi presenti in memoria, ed avremo, ad esempio, MemoryStream;
  • potrebbe trattarsi, invece, di un flusso di dati proveniente dal server a cui siamo collegati, e ci sarà allora, un NetworkStream;
  • e così via, per molti diverse casistiche...

Globalmente parlando, quindi, si può associare uno stream al flusso di dati proveniente da un qualsiasi dispositivo virtuale o fisico o da qualunque entità astratta all'interno della macchina: ad esempio è possibile avere uno stream associato a una stampante, a uno scanner, allo schermo, ad un file, alla memoria temporanea, a qualsiasi altra cosa. Per ognuno di questi casi, esisterà un'opportuna classe derivata di Stream studiata per adempiere a quello specifico compito.
In questo capitolo, vedremo cinque classi del genere, ognuna altamente specializzata: FileStream, StreamReader, StreamWriter, BinaryReader e BinaryWriter.

FileStream

Questa classe offre funzionalità generiche per l'accesso a un file. Il suo costruttore più semplice accetta due parametri: il primo è il percorso del file a cui accedere ed il secondo indica le modalità di apertura. Quest'ultimo parametro è di tipo IO.FileMode, un enumeratore che contiene questi campi:

  • Append : apre il file e si posiziona alla fine (in questo modo, potremo velocemente aggiungere dati senza sovrascrivere quelli precedentemente esistenti);
  • Create : crea un nuovo file con il percorso dato nel primo parametro; se il file esiste già, sarà sovrascritto;
  • CreateNew : crea un nuovo file con il percorso dato nel primo parametro del costruttore; se il file esiste già, verrà sollevata un'eccezione;
  • Open : apre il file e si posiziona all'inizio;
  • OpenOrCreate : apre il file, se esiste, e si posiziona all'inizio; se non esiste, crea il file;
  • Truncate : apre il file, cancella tutto il suo contenuto, e si posiziona all'inizio.

Un terzo parametro opzionale può specificare i permessi (solo lettura, solo scrittura o entrambe), ma per ora non lo useremo.
Prima di vedere un esempio del suo utilizzo, è necessario dire che questa classe considera i file aperti come file binari. Si parla di file binario quando esiste una corrispondenza biunivoca tra i bytes esistenti in esso e i dati letti. Questa condizione non si verifica con i file di testo, in cui, ad esempio, il singolo carattere "a capo" corrisponde a due bytes: in questo caso non si può parlare di file binari, ma è comunque possibile leggerli come tali, e ciò che si otterrà sarà solo una sequenza di numeri. Ma vedremo meglio queste differenze nel paragrafo successivo.
Ora, ammettendo di avere aperto il file, sia che si voglia leggere, sia che si voglia scrivere, sarà necessario adottare un buffer, ossia un array di bytes che conterrà temporaneamente i dati letti o scritti. Tutti i metodi di lettura/scrittura binari del Framework, infatti, richiedono come minimo tre parametri:

  • buffer : un array di bytes in cui porre i dati letti o da cui prelevare i dati da scrivere;
  • index : indice del buffer da cui iniziare l'operazione;
  • length : numero di bytes da processare.

Seguendo questa logica, avremo la funzione Read:

Read(buffer, index, length)

che legge length bytes dallo stream aperto e li pone in buffer (a partire da index); e, parimenti, la funzione Write:

Write(buffer, index, length)

che scrive sullo stream length bytes prelevati dall'array buffer (a partire da index). Ecco un esempio:

Module Module1
 
    Sub Main()
        Dim File As IO.FileStream
        Dim FileName As String
 
        Console.WriteLine("Inserire il percorso di un file:")
        FileName = Console.ReadLine
 
        'IO.File.Exists(path) restituisce True se il percorso
        'path indica un file esistente e False in caso contrario
        If Not IO.File.Exists(FileName) Then
            Console.WriteLine("Questo file non esiste!")
            Console.ReadKey()
            Exit Sub
        End If
 
        Console.Clear()
 
        'Apre il file specificato, posizionandosi all'inizio
        File = New IO.FileStream(FileName, IO.FileMode.Open)
 
        Dim Buffer() As Byte
        Dim Number, ReadBytes As Int32
 
        'Chiede all'utente quanti bytes vuole leggere, e
        'memorizza tale numero in Number
        Console.WriteLine("Quanti bytes leggere?")
        Number = CType(Console.ReadLine, Int32)
        'Se Number è un numero positivo e non siamo ancora
        'arrivati alla fine del file, allora legge quei bytes.
        'La proprietà Position restituisce la posizione
        'corrente all'interno del file (a iniziare da 0), mentre
        'File.Length restituisce la lunghezza del file, in bytes.
        Do While (Number > 0) And (File.Position < File.Length - 1)
            'Ridimensiona il buffer
            ReDim Buffer(Number - 1)
            'Legge Number bytes e li mette in Buffer, a partire
            'dall'inizio dell'array. Read è una funzione, e
            'restituisce come risultato il numero di bytes
            'effettivamente letti dallo stream.
            ReadBytes = File.Read(Buffer, 0, Number)
 
            Console.WriteLine("Bytes letti:")
            For I As Int32 = 0 To ReadBytes - 1
                Console.Write("{0:000} ", Buffer(I))
            Next
            Console.WriteLine()
 
            'Se abbiamo letto tanti bytes quanti ne erano stati
            'chiesti, allora non siamo ancora arrivati alla
            'fine del file. Richiede all'utente un numero
            If ReadBytes = Number Then
                Console.WriteLine("Quanti bytes leggere?")
                Number = CType(Console.ReadLine, Int32)
            End If
        Loop
 
        'Controlla se si è raggiunta la fine del file.
        'Infatti, il ciclo potrebbe terminare anche se l'utente
        'immettesse 0.
        If File.Position >= File.Length - 1 Then
            Console.WriteLine("Raggiunta fine del file!")
        End If
 
        'Chiude il file
        File.Close()
 
        Console.ReadKey()
    End Sub
 
End Module

Bisogna sempre ricordarsi di chiudere il flusso di dati quando si è finito di utilizzarlo. FileStream, e in generale anche Stream, implementa l'interfaccia IDisposable e il metodo Close non è altro che un modo indiretto per richiamare Dispose (a cui, comunque, possiamo fare ricorso). Allo stesso modo, possiamo usare la funzione Write per scrivere dati, oppure WriteByte per scrivere un byte alla volta.
Come avrete notato, la classe Stream espone anche delle proprietà in sola lettura come CanRead, CanWrite e CanSeek. Infatti, non tutti i flussi di dato supportano tutte le operazioni di lettura, scrittura e ricerca: un esempio può essere il NetworkStream (che analizzeremo nella sezione dedicata al Web) associato alle richieste http, il quale non supporta le operazioni di ricerca e restituisce un errore se si prova ad utilizzare il metodo Seek. Questo metodo serve per spostarsi velocemente da una parte all'altra del flusso di dati, e accetta solo due argomenti:

Seek(offset, origin)

offset è un intero che specifica la posizione a cui recarsi, mentre origin è un valore enumerato di tipo IO.SeekOrigin che può assumere tre valori: Begin (si riferisce all'inizio del file), Current (si riferisce alla posizione corrente) ed End (si riferisce alla fine del file). Ad esempio:

'Si sposta alla posizione 100
File.Seek(100, IO.SeekOrigin.Begin)
'Si sposta di 250 bytes indietro rispetto alla posizione corrente
File.Seek(-250, IO.SeekOrigin.Current)
'Si sposta a 100 bytes dalla fine del file
File.Seek(-100, IO.SeekOrigin.End)

Certo che leggere e scrivere dati un byte alla volta non è molto comodo. Vediamo, allora, la prima categoria di file: i file testuali.

Lettura/scrittura di file testuali

I file testuali sono così denominati perchè contengono solo testo, ossia bytes codifcabili in una delle codifiche standard dei caratteri (ASCII, UTF-8, eccetera...). Alcuni particolari bytes vengono intepretati in modi diversi, come ad esempio la tabulazione, che viene rappresentata con uno spazio più lungo; altri vengono tralasciati nella visualizzazione e sembrano non esistere, ad esempio il NULL terminator, che rappresenta la fine di una stringa, oppure l'EOF (End Of File); altri ancora vengono presi a gruppi, come il carattere a capo, che in realtà è formato da una sequenza di due bytes (Carriage Return e Line Feed, rispettivamente 13 e 10). La differenza insita in questi tipi di file rispetto a quelli binari è il fatto di non poter leggere i singoli bytes perchè non ce n'è necessità: quello che importa è l'informazione che il testo porta al suo interno. La classe usata per la lettura è StreamReader, mentre quella per la scrittura StreamWriter: il costruttore di entrambi accetta un unico parametro, ossia il percorso del file in questione; esistono anche altri overloads dei costruttori, ma il più usato e quindi il più importante di tutti è quello appena citato. Ecco un piccolo esempio di come utilizzare tali classi in una semplice applicazione console:

Module Module1
    Sub Main()
        Dim File As String
        Dim Mode As Char
 
        Console.WriteLine("Premere R per leggere un file, W per scriverne uno.")
        'Console.ReadKey restituisce un oggetto ConsoleKeyInfo,
        'al cui interno ci sono tre proprietà: Key,
        'enumeratore che definisce il codice del pulsante premuto; 
        'KeyChar, il carattere corrispondente a quel pulsante; 
        'Modifier, enumeratore che definisce i modificatori attivi, 
        'ossia Ctrl, Shift e Alt. 
        'Quello che serve ora è solo KeyChar
        Mode = Console.ReadKey.KeyChar
        'Dato che potrebbe essere attivo il Bloc Num, ci si 
        'assicura che Mode contenga un carattere maiuscolo 
        'con la funzione statica ToUpper del tipo base Char
        Mode = Char.ToUpper(Mode)
        'Pulisce lo schermo
        Console.Clear()
 
        Select Case Mode
            Case "R"
                Console.WriteLine("Inserire il percorso del file da leggere:")
                File = Console.ReadLine
 
                'Cosntrolla che il file esista
                If Not IO.File.Exists(File) Then
                    'Se non esiste, visualizza un messggio ed esce
                    Console.WriteLine("Il file specificato non esiste!")
                    Console.ReadKey()
                    Exit Sub
                End If
 
                Dim Reader As New IO.StreamReader(File)
 
                'Legge ogni singola riga del file, fintanto che non
                'si è raggiunta la fine
                Do While Not Reader.EndOfStream
                    'Come Console.Readline, la funzione d'istanza 
                    'ReadLine restituisce una linea di testo 
                    'dal file
                    Console.WriteLine(Reader.ReadLine)
                Loop
 
                'Quindi chiude il file
                Reader.Close()
            Case "W"
                Console.WriteLine("Inserire il percorso del file da creare:")
                File = Console.ReadLine
 
                Dim Writer As New IO.StreamWriter(File)
                Dim Line As String
 
                Console.WriteLine("Immettere il testo del file, " & _
                    "premere due volte invio per terminare")
                'Fa immettere righe di testo fino a quando 
                'si termina
                Do
                    Line = Console.ReadLine
                    'Come Console.WriteLine, la funzione d'istanza 
                    'WriteLine scrive una linea di testo sul file
                    Writer.WriteLine(Line)
                Loop While Line <> ""
 
                'Chiude il file
                Writer.Close()
            Case Else
                Console.WriteLine("Comando non valido!")
        End Select
 
        Console.ReadKey()
    End Sub
End Module

Ovviamente esistono anche i metodi Read e Write, che scrivono del testo senza mandare a capo: inoltre, Write e WriteLine hanno degli overloads che accettano anche stringhe di formato come quelle viste nei capitoli precedenti.
Come si è visto, le classi analizzate (e quelle che andremo a vedere tra breve) hanno metodi molti simili a quelli di Console: questo perchè anche la console è uno stream, capace di input e output allo stesso tempo. Per coloro che provengono dal C non sarà difficile richiamare questo concetto.

Lettura/scrittura di file binari

Come già accennato nel paragrafo precedente, la distinzione tra file binari e testuali avviene tramite l'interpretazione dei singoli bytes. Con questo tipo di file, c'è una corrispondenza biunivoca tra i bytes del file e i dati letti dal programma: infatti, non a caso, l'I/O viene gestito attraverso un array di byte. BinaryWriter e BinaryReader espongono, oltre alle canoniche Read e Write già analizzate per FileStream, altre procedure di lettura e scrittura, che, di fatto, scendono a più basso livello. Ad esempio, all'inizio della guida ho illustrato alcuni tipi di dato basilari, riportando anche la loro grandezza (in bytes). Integer occupa 4 bytes, Int16 ne occupa 2, Single he occupa 4 e così via. Valori di tipo base vengono quindi salvati in memoria in notazione binaria, rispettando quella specifica dimensione. Ora, esistono modi ben definiti per convertire un numero in base 10 in una sequenza di bit facilmente manipolabile dall'elaboratore: mi riferisco, ad esempio, alla notazione in complemento a 2 per gli interi e al formato in virgola mobile per i reali. Potete documentarvi su queste modalità di rappresentazione dell'informazione altrove: in questo momento ci interessa sapere che i dati sono "pensati" dal calcolatore in maniera diversa da come li concepiamo noi. BinaryWriter e BinaryReader sono classi appositamente create per far da tramite tra ciò che capiamo noi e ciò che capisce il computer. Proprio perchè sono dei "mezzi", il loro costruttore deve specificare lo stream (già aperto) su cui lavorare. Ecco un esempio:

Module Module1
 
    Sub Main()
        'Apre il file "prova.dat", creandolo o sovrascrivendolo
        Dim File As New IO.FileStream("prova.dat", IO.FileMode.Create)
        'Writer è lo strumento che ci permette di scrivere
        'sullo stream File con codifica binaria
        Dim Writer As New IO.BinaryWriter(File)
        Dim Number As Int32
 
        Console.WriteLine("Inserisci 10 numeri da scrivere sul file:")
        For I As Int32 = 1 To 10
            Console.Write("{0}: ", I)
            Number = CType(Console.ReadLine, Int32)
            Writer.Write(Number)
        Next
        Writer.Close()
 
        Console.ReadKey()
    End Sub
 
End Module

Io ho inserito questi numeri: -10 -5 0 1 20 8000 19001 -345 90 22. Provando ad aprire il file con un editor di testo vedrete solo caratteri strani, in quanto questo non è un file testuale. Aprendolo, invece, con un editor esadecimale, otterrete questo:

f6 ff ff ff fb ff ff ff 00 00 00 00 01 00 00 00
14 00 00 00 40 lf 00 00 39 4a 00 00 a7 fe ff ff
5a 00 00 00 16 00 00 00

Ogni gruppetto di quattro bytes rappresenta un numero intero codificato in binario. Potremmo fare la stessa cosa con Single, Double, Date, Boolean, String e altri tipi base per vedere cosa succede.
BinaryWriter e BinaryReader sono molto utili quando bisogna leggere dati in codifica binaria, ad esempio per molti famosi formati di file, come mp3, wav (vedi sezione FFS), zip, mpg, eccetera...

Esempio: steganografia su immagini

La steganografia è l'arte di nascondere del testo all'interno di un'immagine. Per i più curiosi, mi avventurerò nella scrittura di un semplicissimo programma di steganografia su immagini, nascondendo del testo al loro interno.
Per prima cosa, si costruisca l'interfaccia grafica, con questi controlli:

  • Una Label, Label1, Text = "Introdurre il percorso di un'immagine:"
  • Una TextBox, txtPath, cone AutoCompleteMode = Suggest e AutoCompleteSource = FileSystem. In questo modo, la textbox suggerirà il nome di file e cartelle esistenti mentre state digitando, rendendo più semplice l'indtroduzione del percorso;
  • Una TextBox, txtText, ScrollBars = Both, MultiLine = True
  • Un Button, btnHide, Text = "Nascondi"
  • Un Button, btnRead, Text = "Leggi"

IOSteganos.jpg
Ed ecco il codice ampiamente commentato:

Public Class Form1
 
    Private Sub btnHide_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnHide.Click
        If Not IO.File.Exists(txtPath.Text) Then
            MessageBox.Show("File inesistente!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error)
            Exit Sub
        End If
 
        If IO.Path.GetExtension(txtPath.Text) <> ".jpg" Then
            MessageBox.Show("Il file deve essere in formato JPEG!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
            Exit Sub
        End If
 
        Dim File As New IO.FileStream(txtPath.Text, IO.FileMode.Open)
        'Converte il testo digitato in una sequenza di bytes,
        'secondo gli standard della codifica UTF8
        Dim TextBytes() As Byte = _
            System.Text.Encoding.UTF8.GetBytes(txtText.Text)
 
        'Va alla fine del file
        File.Seek(0, IO.SeekOrigin.End)
        'Scrive i bytes
        File.Write(TextBytes, 0, TextBytes.Length)
        File.Close()
 
        MessageBox.Show("Testo nascosto con successo!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Information)
    End Sub
 
    Private Sub btnRead_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRead.Click
        If Not IO.File.Exists(txtPath.Text) Then
            MessageBox.Show("File inesistente!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Error)
            Exit Sub
        End If
 
        If IO.Path.GetExtension(txtPath.Text) <> ".jpg" Then
            MessageBox.Show("Il file deve essere in formato JPEG!", Me.Text, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
            Exit Sub
        End If
 
        Dim File As New IO.FileStream(txtPath.Text, IO.FileMode.Open)
        Dim TextBytes() As Byte
        Dim B1, B2 As Byte
 
        'Legge un byte
        B1 = File.ReadByte()
        Do
            'Legge un altro byte
            B2 = File.ReadByte()
            'Se i bytes formano la sequenza FF D9, si ferma.
            'In Visual Basic, in numeri esadecimali si scrivono
            'facendoli precedere da "&H"
            If B1 = &HFF And B2 = &HD9 Then
                Exit Do
            End If
            'Passa il valore di B2 in B1
            B1 = B2
        Loop While (File.Position < File.Length - 1)
 
        ReDim TextBytes(File.Length - File.Position - 1)
        'Legge ciò che rimane dopo FF D9
        File.Read(TextBytes, 0, TextBytes.Length)
        File.Close()
 
        txtText.Text = System.Text.Encoding.UTF8.GetString(TextBytes)
    End Sub
End Class

Il testo accodato può essere rilevato facilmente con un Hex Editor, per questo lo si dovrebbe criptare con una password: per ulteriori informazioni sulla criptazione in .NET, vedere capitolo rekativo.

<< Precedente Prossimo >>
A proposito dell'autore

C#, TypeScript, java, php, EcmaScript (JavaScript), Spring, Hibernate, React, SASS/LESS, jade, python, scikit, node.js, redux, postgres, keras, kubernetes, docker, hexo, etc...