1. Introduzione
Immagina che dobbiamo di nuovo leggere il contenuto di un file di testo. Supponiamo di avere un file con citazioni utili di programmatori che vogliamo mostrare nella nostra app di testo già familiare. Sì, abbiamo già visto metodi di alto livello come File.ReadAllText, ma se il file è grande e dobbiamo leggerlo non tutto insieme, ma riga per riga? O magari vogliamo leggere il file in modo controllato, tipo caricare le righe lentamente per non far esplodere la memoria?
Ed è qui che StreamReader entra in gioco — una classe perfetta per leggere file di testo riga per riga, a blocchi, carattere per carattere — insomma, flessibile e comoda.
Come funziona StreamReader?
StreamReader è una classe dello spazio dei nomi System.IO che legge dati di testo da uno stream (di solito un file, ma può essere anche uno stream di rete o memoria). Sa leggere i dati con l'encoding giusto, li trasforma in caratteri e stringhe, e ti restituisce tipi di dati comodi: stringhe e caratteri.
Se lo disegniamo come uno schema, viene fuori qualcosa tipo:
[ File (bytes) ] --(FileStream)--> [ StreamReader (decodifica caratteri) ] ---> [ Il tuo codice (string, char) ]
- File (o altra sorgente di byte): Ci dà i byte.
- FileStream: Legge questi byte come un "canale".
- StreamReader: Trasforma i byte in caratteri e stringhe secondo l'encoding (di default — UTF-8).
Perché non sempre conviene usare solo File.ReadAllText?
Nella pratica i file possono essere grandi. Anche molto grandi (tipo log o csv con un milione di righe). Cercare di leggere tutto il file in memoria è come provare a mangiare tutta la torta in una volta: buono, ma rischioso per la salute.
StreamReader legge il file "a pezzi". Non carica tutto il file in memoria, ma carica e restituisce la riga o il carattere solo quando serve. È economico e super comodo.
2. Lettura del file intero riga per riga
Creiamo un file quotes.txt con queste righe:
Il codice è poesia.
Il debug è un detective.
"Non funziona" è il miglior bug report.
Ora aggiungiamo il codice alla nostra app didattica (facciamo che sia una semplice app console che stiamo sviluppando piano piano). Mettiamo il file vicino al .exe — così il programma lo legge e mostra il contenuto riga per riga.
Codice con commenti:
// Otteniamo il percorso del file nella cartella dell'app
string fileName = "quotes.txt";
string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
// Controlliamo se il file esiste
if (!File.Exists(filePath))
{
Console.WriteLine($"File non trovato: {filePath}");
return;
}
// Apriamo il file per la lettura usando StreamReader
using StreamReader reader = new StreamReader(filePath);
string? line;
int lineNumber = 1;
// Leggiamo il file riga per riga fino alla fine
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"{lineNumber,2}: {line}");
lineNumber++;
}
Cosa succede qui?
- Calcoliamo il percorso del file con Path.Combine e AppDomain.CurrentDomain.BaseDirectory — così il codice funziona uguale su Windows, Linux e anche in Docker.
- Prima controlliamo che il file esista davvero.
- Con using creiamo uno StreamReader e leggiamo il file riga per riga (metodo ReadLine()).
- Alla fine del blocco using il file viene chiuso, anche se ti dimentichi di farlo a mano o se becchi un'eccezione.
Visualizzazione: schema di lavoro
[ quotes.txt ] --(FileStream)--> [ StreamReader ] --(ReadLine)--> [ string line ] --(Console.WriteLine)--> Schermo
^
|
AppDomain.CurrentDomain.BaseDirectory + Path.Combine
3. Caratteristiche e particolarità d'uso
Come avviene la lettura delle righe
Il metodo ReadLine() restituisce la stringa fino al primo carattere di nuova riga (\n o \r\n). Quando il file finisce, restituisce null. Perciò il ciclo di solito è così:
string? line;
while ((line = reader.ReadLine()) != null)
{
// elaborazione della riga
}
Errori tipici con StreamReader
A volte viene voglia di non usare using, ma scrivere così:
StreamReader reader = new StreamReader(filePath);
// ...
reader.Close();
Questa cosa è come dimenticarsi la porta aperta col freddo: può succedere di tutto. Se durante la lettura c'è un errore (tipo il disco non è più accessibile), il codice per chiudere il file non viene eseguito e il file resta "appeso" aperto nel sistema.
Perciò usare using non è un consiglio, ma una vera necessità. Il tuo futuro collega sysadmin ti ringrazierà che il tuo programma non lascia una giungla di file "bloccati".
Scrittura corta con using declaration
Con le versioni moderne di C# (da 8.0 in su) puoi scrivere più corto:
using StreamReader reader = new StreamReader(filePath);
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
Dispose viene chiamato alla fine del blocco corrente (del metodo).
4. Dettagli utili
Lettura di file con encoding sconosciuto
Di default StreamReader lavora con UTF-8. Ma a volte trovi file con altro encoding (tipo Windows-1251 per vecchi testi in russo). In quel caso il programma può mostrare "?" o caratteri illeggibili.
Puoi specificare l'encoding in modo esplicito:
using System.Text;
// Apriamo il file come Windows-1251
using StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding("windows-1251"));
Documentazione su encoding e Encoding
Lettura del file come un unico grande testo
A volte serve ottenere tutto il file come un unico testo, ma non tramite File.ReadAllText, ma con StreamReader.
using StreamReader reader = new StreamReader(filePath);
string allText = reader.ReadToEnd();
Console.WriteLine(allText);
- Il metodo ReadToEnd() legge tutto il contenuto del file dalla posizione attuale fino alla fine come una sola stringa.
- Per file grandi può essere inefficiente e c'è il rischio di OutOfMemoryException. Per file fino a qualche megabyte — va benissimo.
Come si usa nella vita reale
- Gestione dei log. Se hai un file di log server, è comodo leggerlo riga per riga, filtrare gli eventi che ti servono e non caricare tutto in memoria.
- Import CSV. Se devi parsare tabelle grandi, è comodo prendere i dati riga per riga e processarli man mano.
- Controllo di parole chiave in file di testo grandi. Puoi cercare il testo che ti serve tra milioni di righe senza paura di saturare la memoria.
- Unit test. I file con dati di test spesso si leggono riga per riga con StreamReader.
Confronto tra i metodi di lettura
| Metodo | Quando usarlo | Vantaggi | Svantaggi |
|---|---|---|---|
|
File piccoli | Una riga di codice, veloce | File grande – tanta memoria |
|
Qualsiasi file, soprattutto grandi | Lettura per righe, poca memoria | Un po' più di codice, logica più complessa |
|
File piccoli/medi | Gestione flessibile dell'encoding | Difficile con file molto grandi |
5. Pratica
Complichiamo un po' il compito. Facciamo che il nostro programma mostri solo le prime N righe del file, dove il numero lo inserisce l'utente. Dai, a volte non vuoi vedere subito centinaia di righe — bastano un paio di citazioni per motivarti. :)
Ecco come si fa:
using System;
using System.IO;
class Program
{
static void Main()
{
string fileName = "quotes.txt";
string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
if (!File.Exists(filePath))
{
Console.WriteLine($"File non trovato: {filePath}");
return;
}
Console.Write("Quante righe vuoi mostrare? ");
string? input = Console.ReadLine();
if (!int.TryParse(input, out int linesToShow) || linesToShow < 1)
{
Console.WriteLine("Errore: Inserisci un numero valido maggiore di 0.");
return;
}
using StreamReader reader = new StreamReader(filePath);
int current = 0;
string? line;
while (current < linesToShow && (line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
current++;
}
if (current == 0)
Console.WriteLine("Il file è vuoto o non sono riuscito a leggere la prima riga.");
else if (current < linesToShow)
Console.WriteLine($"Nel file c'erano solo {current} riga(e).");
}
}
6. Momenti importanti ed errori tipici
Quando lavori con StreamReader, la cosa principale è ricordarsi di gestire bene le risorse (vedi la lezione precedente su IDisposable). Se te lo dimentichi, puoi beccarti l'errore "file già usato da un altro processo" o "troppi file aperti".
Un'altra cosa: attenzione all'encoding. Se non sei sicuro che il file sia in UTF-8, specifica l'encoding giusto. C'è un secondo parametro nel costruttore di StreamReader apposta.
Un altro "tranello" pratico: se il file viene aggiornato mentre il programma gira (tipo un file di log). Non sempre puoi vedere subito le nuove righe se il file è usato da altri processi — allora devi ricalcolare/rileggere il file da capo o usare metodi speciali, ma di questo parleremo più avanti.
GO TO FULL VERSION