CodeGym /Corsi /C# SELF /Lettura e scrittura file in modo sicuro

Lettura e scrittura file in modo sicuro

C# SELF
Livello 38 , Lezione 4
Disponibile

1. Introduzione

Nel programming la sicurezza nel lavorare con i file non significa solo protezione da virus e hacker, ma anche gestione corretta di errori, lock, permessi, percorsi non validi e altre insidie. Un approccio negligente può portare a perdita di dati, blocchi dell'applicazione, bug strani o alla famosa eccezione IOException.

Ecco le situazioni classiche che vogliamo saper gestire:

  • Il file non esiste ma proviamo a leggerlo (o al contrario: è già creato e vogliamo scrivere in modalità "create only if not exists").
  • Il file è aperto da un altro programma e bloccato.
  • L'utente non ha permessi di lettura o scrittura su quel file o cartella.
  • Il percorso al file è sbagliato o contiene caratteri proibiti.
  • L'operazione viene interrotta inaspettatamente (per esempio, disco pieno).
  • Pratica scorretta: lasciare file aperti, non chiudere gli stream.

Per fortuna, .NET fornisce tutti gli strumenti per risolvere questi problemi. Sono semplici — ma come le cinture di sicurezza, l'importante è ricordarsi di allacciarle.

2. Principi base per lavorare in sicurezza con i file

Controlla in anticipo l'esistenza del file e i permessi

Prima di leggere un file, verifica che esista (per esempio con File.Exists), specialmente se il percorso viene dall'utente:

string path = "test.txt";
if (!File.Exists(path))
{
    Console.WriteLine("Errore: file non trovato.");
    return;
}

Prima di scrivere assicurati che la directory esista e che tu abbia i permessi di scrittura (o risali l'errore a un livello superiore).

Non lasciare mai gli stream aperti

Usa la keyword using per chiudere automaticamente gli stream — è best practice:

using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// Il file è già chiuso qui, anche se si verifica un errore!

Questo protegge da lock "incastrati" e leak di risorse.

Intercetta sempre le eccezioni

Qualsiasi operazione su file può lanciare un'eccezione. Anche se il file era appena presente — potrebbe essere cancellato/spostato al momento dell'accesso. Usiamo la costruzione try-catch:

try
{
    using var reader = new StreamReader("data.txt");
    string line = reader.ReadLine();
    Console.WriteLine(line);
}
catch (FileNotFoundException)
{
    Console.WriteLine("File non trovato.");
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("Accesso al file negato.");
}
catch (IOException ex)
{
    Console.WriteLine($"Errore di I/O: {ex.Message}");
}

Non fidarti dell'input dell'utente

Se il percorso è fornito dall'utente, potrebbe essere sbagliato (o addirittura voler crashare la tua app). È consigliato validare il percorso (vedi Path.GetInvalidPathChars()):

try
{
    string userPath = Console.ReadLine()!;
    if (string.IsNullOrWhiteSpace(userPath))
    {
        Console.WriteLine("Il percorso non può essere vuoto!");
        return;
    }
    // In più: controllare i caratteri proibiti
    foreach (char c in Path.GetInvalidPathChars())
        if (userPath.Contains(c))
        {
            Console.WriteLine("Il percorso contiene caratteri non validi.");
            return;
        }
    // Poi si può lavorare in sicurezza col file
}
catch (Exception ex)
{
    Console.WriteLine("Errore nella validazione del percorso: " + ex.Message);
}

3. Esempio pratico: leggiamo un file "da adulti"

Facciamo un upgrade al nostro progetto didattico. Immaginiamo di dover leggere un file il cui nome è inserito dall'utente e stamparne il contenuto. Il tutto con gestione degli errori e attenzione alla codifica.

Console.Write("Inserisci il percorso del file: ");
string? path = Console.ReadLine();

// Validazione del percorso
if (string.IsNullOrWhiteSpace(path))
{
    Console.WriteLine("Il percorso non può essere vuoto!");
    return;
}
foreach (char c in Path.GetInvalidPathChars())
    if (path.Contains(c))
    {
        Console.WriteLine("Il percorso contiene caratteri non validi.");
        return;
    }

// Proviamo a leggere il file
try
{
    if (!File.Exists(path))
    {
        Console.WriteLine("File non trovato.");
        return;
    }

    // Specificare esplicitamente la codifica (es. UTF-8)
    using var reader = new StreamReader(path, Encoding.UTF8);
    string content = reader.ReadToEnd();
    Console.WriteLine("Contenuto del file:");
    Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("Nessun permesso per leggere il file.");
}
catch (IOException ex)
{
    Console.WriteLine("Errore di I/O: " + ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine("Errore imprevisto: " + ex.Message);
}

Nota che questo codice non crasha se il file scompare tra l'inserimento del nome e la lettura: l'eccezione viene catturata. Lo stream viene chiuso automaticamente all'uscita dal blocco using — anche in caso di errore.

4. File in scrittura: evitiamo perdita di dati

Quando apriamo un file per scrittura, specialmente in modalità overwrite (false nel secondo parametro del costruttore di StreamWriter), c'è il rischio di sovrascrivere dati importanti. Ecco qualche consiglio:

Controlla se stai sovrascrivendo un file esistente

Talvolta conviene chiedere conferma all'utente se il file esiste già:

if (File.Exists(path))
{
    Console.WriteLine("Attenzione: il file esiste già. Sovrascrivere? (y/n)");
    string answer = Console.ReadLine()!;
    if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
        return;
}

Usa la modalità "append" quando serve

using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": nuova voce nel log.");

Le informazioni vecchie non verranno cancellate.

5. Protezione da race condition e conflitti

A volte il file può essere aperto contemporaneamente da più programmi (per esempio il tuo client C# e Notepad++). Questo può causare errori. Di default StreamReader e StreamWriter usano modalità di condivisione basate su FileShare — cioè permettono o vietano l'accesso ad altri.

Si può controllare questo comportamento esplicitamente:

using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// Lettura...
  • FileShare.Read: altri possono solo leggere.
  • FileShare.None: nessuna condivisione — il file è tutto "tuo".

Se serve che il file sia contemporaneamente leggibile e scrivibile da più programmi, usa il corrispondente flag, ma fallo solo se capisci bene le conseguenze.

6. Eccezioni inaspettate e come gestirle

Anche seguendo tutte le buone pratiche possono capitare guasti legati a spazio esaurito, chiavetta corrotta o virus. Ecco alcune eccezioni "esotiche" e come intercettarle:

  • PathTooLongException — il percorso è troppo lungo (più di 260 caratteri nelle vecchie versioni di Windows).
  • DirectoryNotFoundException — la directory specificata non è stata trovata.
  • DriveNotFoundException — per esempio se il percorso è "Z:\\file.txt" ma l'unità Z non esiste.
  • NotSupportedException — per esempio il percorso contiene una combinazione non supportata.

È consigliabile avere blocchi separati per queste eccezioni — o almeno loggarle in modo distinto.

7. Usare file temporanei per scrittura atomica

Il problema classico: scriviamo un file ma l'app va in crash a metà — otteniamo un file corrotto. I programmi professionali spesso usano la strategia della scrittura "atomica":

  1. Scrivere il contenuto in un file temporaneo (per esempio "file.txt.tmp").
  2. Spostare il file temporaneo (operazione di solito atomica a livello di file system) sopra quello di destinazione (File.Replace).
  3. Il file vecchio viene sostituito completamente oppure rimane invariato — non ci sono dati "a metà".

Esempio:

string tempPath = path + ".tmp";
try
{
    using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
    // Scriviamo tutto nel file temporaneo
    writer.Write(contentForSave);
    // Dopo la scrittura riuscita sostituiamo il file principale
    File.Replace(tempPath, path, null); // Muove il file temporaneo sostituendo quello target (operazione atomica)
}
catch (Exception ex)
{
    Console.WriteLine("Errore nel salvataggio del file: " + ex.Message);
    // È buona norma cancellare tempPath se non serve
}

Nella pratica editor e suite office usano questo approccio per garantire la consistenza dei dati.

8. Usare wrapper per accesso sicuro

.NET offre helper per operazioni "sicure" sui file. Per esempio File.ReadAllText e File.WriteAllText aprono, leggono/scrivono e chiudono il file automaticamente. Anche loro però vanno avvolti in un try-catch:

try
{
    string text = File.ReadAllText("settings.json", Encoding.UTF8);
    // Lavoriamo con i dati...
}
catch (Exception ex)
{
    Console.WriteLine("Errore nell'operazione sul file: " + ex.Message);
}

Per file grandi usa gli stream e leggi a pezzi, così non consumi tutta la memoria.

1
Sondaggio/quiz
Eccezioni tipiche, livello 38, lezione 4
Non disponibile
Eccezioni tipiche
Gestione degli errori quando lavori con i file
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION