1. Introduzione: perché i file fanno i capricci?
Capita che apri un documento — e subito il sistema ti dice: "File non trovato". Oppure un tentativo di salvataggio si conclude con il messaggio: "Accesso negato". Sono proprio quei casi in cui coi file succede qualcosa di strano. Se con le codifiche vediamo i soliti "caratteri illeggibili", qui i problemi sono legati al file system stesso.
Puoi immaginare il file system come una grande biblioteca e l'applicazione come il bibliotecario. Quando chiede un file, ci sono varie possibili risposte:
- Il libro (cioè il file) non c'è — semplicemente non esiste.
- La sezione della biblioteca dove dovrebbe esserci il libro non esiste — manca la directory necessaria.
- Il libro è "incatenato" o è stato preso da un altro — il file è usato da un altro processo o non ci sono permessi di accesso.
- Lo scaffale è pieno — non c'è spazio sul disco per creare un nuovo file.
- Oppure, per esempio, provi a mettere il libro nel reparto restituzioni DVD — cioè stai facendo un'operazione non supportata.
In C# tutte queste situazioni si rappresentano con eccezioni. Il compito dello sviluppatore non è solo osservare il programma che "cade", ma prevedere i possibili problemi e gestirli con cura. Difficilmente qualcuno vuole visualizzare un messaggio di errore oscuro pieno di testo incomprensibile.
Siamo già familiari con la costruzione try-catch. È la nostra corda di salvezza che ci permette di "catturare" l'eccezione e reagire, invece di lasciare che il programma finisca in crash.
// Questo è il nostro vecchio amico, un promemoria dalla Lezione 57
try
{
// Qui scriviamo il codice che può causare un errore
// Per esempio, un tentativo di lettura di un file
}
catch (Exception ex) // Catturiamo qualsiasi eccezione
{
// Qui gestiamo l'errore
Console.WriteLine($"Oh, si è verificato un errore: {ex.Message}");
}
Oggi entriamo più in dettaglio sulle eccezioni specifiche che si verificano durante il lavoro con i file. Questo ci permetterà di scrivere codice più robusto, capace di "dialogare" con il file system anche quando fa i capricci.
Le eccezioni non sono bug, sono segnali di soccorso!
È importante capire: un'eccezione non è sempre un errore nel tuo codice. Spesso è un segnale che qualcosa è andato storto nell'ambiente esterno con cui il tuo codice interagisce. Il file system è un ottimo esempio di tale ambiente esterno. Puoi scrivere codice perfetto per leggere un file, ma se l'utente cancella il file prima che il programma lo legga, otterrai un'eccezione. E va bene così! Il tuo compito come sviluppatore è insegnare al programma come reagire a queste situazioni.
Vediamo i segnali di soccorso più comuni che potresti incontrare quando lavori con i file.
2. FileNotFoundException: il file non c'era proprio
Questa è probabilmente l'eccezione più comune quando lavori con i file. Si verifica quando provi ad aprire, leggere o fare altre operazioni su un file che non esiste nel percorso indicato.
Esempio di vita reale: chiedi a un amico di portarti il libro "Programmazione in C# 14 per principianti" dalla sua biblioteca, e lui risponde: "Non ho quel libro". Allo stesso modo il tuo programma può chiedere al sistema operativo: "Dammi il file settings.txt", e il sistema risponderà: "Mi dispiace, non c'è".
Proviamo a scrivere del codice che legge il file abracadabra.txt per la nostra app di task manager. Se il file non esiste, dobbiamo informare l'utente invece di "crashare".
try
{
using var reader = new StreamReader("abracadabra.txt");
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException ex)
{
Console.WriteLine("File non trovato: " + ex.FileName);
}
Nella vita quotidiana è come arrivare alla fermata e scoprire che l'autobus non c'è — e neanche il minibus. Tristezza.
Nota: Spesso questa eccezione è accompagnata anche da un percorso sbagliato (per esempio ti sei dimenticato che stai lavorando da una directory diversa).
3. DirectoryNotFoundException: le cartelle sono sparite
Questa eccezione è molto simile a FileNotFoundException, ma riguarda non il file in sé, bensì la directory in cui il file dovrebbe trovarsi. Se indichi un percorso, per esempio, "C:\MyDocuments\MyProject\Data\report.txt", e la cartella Data non esiste, otterrai DirectoryNotFoundException.
Nella nostra app, se volessimo salvare le impostazioni in una sottocartella data, per esempio "./data/app_settings.txt", e quella cartella non esistesse, al tentativo di scrittura o lettura ci imbatteremmo in questo problema.
DirectoryNotFoundException può essere catturata separatamente, come FileNotFoundException, oppure può essere parte della più generale IOException, della quale parleremo dopo.
try
{
using var writer = new StreamWriter(@"C:\very\strange\path\file.txt");
writer.WriteLine("Hello world");
}
catch (DirectoryNotFoundException ex)
{
Console.WriteLine("Directory non trovata!");
}
Errore comune: La cartella può essere cancellata in qualsiasi momento (per esempio qualcuno rimuove file temporanei), o hai impostato un percorso di scrittura errato.
4. UnauthorizedAccessException: accesso vietato!
Immagina di voler mettere un libro su uno scaffale con il cartello "Accesso solo per il personale". Questo è UnauthorizedAccessException! Si verifica quando il tuo programma non ha i permessi necessari per accedere a un file o a una directory. Le cause possono essere:
- Permessi utente insufficienti: stai cercando di scrivere in una cartella dove solo l'amministratore può scrivere (per esempio C:\Windows).
- File marcato come "sola lettura": stai cercando di modificare un file con attributo di sola lettura.
- File di sistema o nascosto: ci sono restrizioni specifiche su questi file.
Questo è molto comune negli ambienti aziendali o quando gli utenti installano programmi in directory protette.
Proviamo a scrivere un file in una cartella di sistema dove l'utente normale non ha permessi. (Attenzione: eseguire questo codice con cautela per non riempire directory di sistema, meglio farlo in una sandbox).
try
{
using var writer = new StreamWriter("/system/settings.conf");
writer.WriteLine("Tutto il potere agli studenti!");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Nessun accesso al file o alla directory!");
}
Se esegui questo codice senza privilegi di amministratore, molto probabilmente vedrai il messaggio "Accesso a questa cartella negato". Se lo esegui come amministratore, il file probabilmente verrà creato. Questo esempio mostra quanto sia importante gestire UnauthorizedAccessException, così l'utente capisce perché l'app non può eseguire l'operazione.
5. IOException: il generico "oh-oh" del file system
IOException è l'eccezione più generale legata alle operazioni di I/O. Viene lanciata quando c'è qualche problema con il dispositivo di input/output o con il file system che non rientra in eccezioni più specifiche come FileNotFoundException o UnauthorizedAccessException.
Scenari tipici in cui puoi ottenere IOException:
- File già in uso da un altro programma: per esempio cerchi di eliminare un file aperto in Notepad o in un altro processo.
- Disco pieno: non c'è spazio sufficiente per scrivere il file.
- File system o file corrotti: più raro, ma possibile.
- Problemi di rete: se il file è su un drive di rete e la connessione cade.
- Nome file o percorso troppo lunghi. (Questo può essere anche PathTooLongException, ma a volte rientra in IOException).
IOException è una specie di "chiave universale" per molti problemi. Quando catturi un'IOException, è utile guardare anche la proprietà Message per ottenere informazioni più dettagliate su cosa è andato storto.
try
{
using var file = new FileStream("busyfile.txt", FileMode.Open, FileAccess.ReadWrite, FileShare.None);
// in qualche modo teniamo il file aperto
// contemporaneamente da un'altra parte:
using var writer = new StreamWriter("busyfile.txt");
writer.WriteLine("Tentativo di scrittura...");
}
catch (IOException ex)
{
Console.WriteLine("Errore di I/O: " + ex.Message);
}
Nota importante: IOException è la classe base per molte altre eccezioni "file-related".
6. Altri, ma non meno importanti, "fastidi"
PathTooLongException
È un problema meno comune, ma comunque esistente: il tuo percorso (o il nome del file/directory) è troppo lungo per il sistema operativo. Per esempio, se decidessi di mettere nel nome del file il riassunto de "Guerra e pace", Windows non te lo perdonerà.
Su Windows c'è un limite storico di 260 caratteri per il percorso completo. Le versioni più recenti dell'OS e .NET permettono i "long path", ma non sempre funzionano di default.
try
{
string veryLongPath = new string('a', 300); // 300 caratteri!
using var writer = new StreamWriter(veryLongPath + ".txt");
writer.WriteLine("Questo nome di file è troppo lungo!");
}
catch (PathTooLongException ex)
{
Console.WriteLine("Nome file o percorso troppo lunghi!");
}
NotSupportedException
È un caso raro, ma "surreale", quando passi al costruttore di tipi come StreamReader o FileStream una stringa di percorso non valida, per esempio con caratteri proibiti o usi percorsi "magici" tipo C:::\wow???\file.txt.
7. Dettagli utili
Quali eccezioni corrispondono a cosa
| Eccezione | Motivo | Esempio di situazione |
|---|---|---|
|
File non trovato | Apertura di un file inesistente |
|
Directory non trovata | Apertura di un file in una cartella rimossa |
|
Accesso negato (permessi) | Scrittura in una directory protetta |
|
Errore generico I/O | File aperto da un altro processo |
|
Percorso troppo lungo | Nome file/cartella troppo lungo |
|
Formato percorso non supportato | Percorso con caratteri proibiti |
Errori comuni e casi speciali
Esempio: File occupato da un altro processo
Immagina di aver aperto un file di testo in Notepad e aver dimenticato di chiuderlo. In quel momento il tuo programma prova a scrivere nello stesso file. Qui arriva IOException (o anche una "sharing violation").
Esempio: Mancanza di permessi
Prova a salvare dei dati in C:\Windows senza permessi di amministratore — otterrai UnauthorizedAccessException. Succede la stessa cosa se apri un file in sola lettura e provi a scriverci.
Esempio: Percorso non valido
Su Windows non puoi avere nomi di file con i caratteri <>:"/\|?*. Se provi a creare un file con questi caratteri, il programma lancerà NotSupportedException (o ArgumentException).
Esempio: Disco pieno
In modo sorprendente, anche questo genera IOException — per esempio quando il disco è pieno (ecco perché ogni tanto è bene ricordarsi della cartella Downloads).
Come non impazzire: best practice per rilevare errori
- Controlla l'esistenza del file con File.Exists e di una directory con Directory.Exists prima di aprire il file. Ma attenzione: il file può sparire o comparire subito dopo il controllo (classica race condition).
- Non "ingoiare" le eccezioni (non fare semplicemente catch { }) a meno che tu non stia scrivendo un logger rigoroso. Almeno registra o mostra all'utente cosa è successo.
- Cerca di catturare eccezioni specifiche (FileNotFoundException, DirectoryNotFoundException) e non solo il generico Exception.
- Per applicazioni cross-platform considera che permessi, formati dei percorsi, lunghezze dei nomi variano tra Windows, Linux e macOS.
- Se lavori con file di testo, specifica sempre esplicitamente la codifica — così eviti sorprese.
- Per operazioni bulk sui file usa una gestione che tenga conto dei possibili fallimenti ad ogni passo.
Riepilogo rapido delle eccezioni tipiche
| Eccezione | Quando succede? | Come prevenirla/gestirla? |
|---|---|---|
|
File non presente nel percorso indicato | Controlla con File.Exists o crealo |
|
Il percorso contiene una directory inesistente | Verifica il percorso, crea le directory con Directory.CreateDirectory |
|
Nessun permesso su file/cartella, file in sola lettura, occupato da un altro processo | Esegui con l'utente giusto, verifica ACL, chiudi correttamente i file |
|
Errore generico I/O, file occupato, disco pieno | Usa try-catch, evita di tenere file aperti |
|
Percorso o nome file troppo lungo | Accorcia il percorso, usa percorsi relativi |
|
Formato percorso non valido | Verifica la stringa del percorso per caratteri proibiti |
Flowchart "Cosa fare quando c'è un errore con un file?"
flowchart TD
A[Operazione sul file] --> B{È stata lanciata un'eccezione?}
B -- No --> C[Operazione completata con successo]
B -- Sì --> D{Che tipo di eccezione?}
D -- FileNotFound --> E[Chiedere all'utente il file corretto o crearlo]
D -- DirectoryNotFound --> F[Creare la directory mancante]
D -- UnauthorizedAccess --> G[Chiedere all'utente di riavviare con i permessi corretti]
D -- IOException --> H[Controllare chi sta usando il file, controllare il disco]
D -- PathTooLong --> I[Accorciare il percorso]
D -- NotSupported --> J[Verificare il formato del percorso]
D -- Other --> K[Mostrare un messaggio e ricontrollare i log]
8. Come appaiono le eccezioni tipiche in un'app reale?
Sviluppando il nostro progetto didattico, supponiamo di avere una mini-app che salva le note dell'utente in un file e poi le legge.
string notesPath = "notes.txt";
Console.Write("Inserisci una nota: ");
string note = Console.ReadLine();
try
{
// Salviamo la nota
using var writer = new StreamWriter(notesPath, true, Encoding.UTF8);
writer.WriteLine(note);
// Leggiamo tutte le note
Console.WriteLine("Le tue note:");
using var reader = new StreamReader(notesPath, Encoding.UTF8);
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException)
{
Console.WriteLine("File delle note non trovato. Prova a crearlo manualmente.");
}
catch (DirectoryNotFoundException)
{
Console.WriteLine("Il percorso del file delle note non è corretto. Controlla se la cartella esiste.");
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("Nessun permesso per scrivere o leggere il file. Esegui il programma come amministratore.");
}
catch (IOException ex)
{
Console.WriteLine("Si è verificato un errore di I/O: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("Errore inaspettato: " + ex.Message);
}
In questo esempio vediamo una situazione tipica per qualsiasi applicazione: provare a salvare dati su file e poi leggerli. Ogni blocco catch gestisce una classe di errori specifica — dall'assenza del file o della cartella ai problemi di permessi e agli errori generici di I/O. Così l'app non "cade" al primo imprevisto, ma informa l'utente su cosa è andato storto e cosa si può fare. Questo approccio rende l'app più robusta e user-friendly.
GO TO FULL VERSION