Il namespace System.IO contiene tutto quello che serve per lavorare con i file. Per la maggior parte dei compiti basta una chiamata sulla classe statica File: apre il file, lo legge o lo scrive, e lo chiude. Per i file grandi o per un controllo più fine, usa StreamReader e StreamWriter. Aggiungi using System.IO; a ogni esempio qui sotto.
Scrivere e leggere un file intero
File.WriteAllText crea un file (o sovrascrive uno esistente) con il testo che gli passi. File.ReadAllText restituisce l'intero file come una sola stringa. File.AppendAllText aggiunge in fondo:
Output:
True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False
Un percorso relativo come "notes.txt" viene risolto rispetto alla cartella corrente del processo, che non è sempre la cartella in cui si trova il programma (un servizio, un'attività pianificata o un test runner possono partire altrove). Per i file distribuiti con l'applicazione, costruisci il percorso a partire da AppContext.BaseDirectory.
WriteAllText sovrascrive senza chiedere. Se perdere il contenuto precedente è un problema, controlla prima File.Exists oppure scrivi in un file temporaneo e poi spostalo al suo posto con File.Move.
Righe: WriteAllLines, ReadAllLines e ReadLines
I file di record, uno per riga, sono così comuni da avere metodi dedicati. WriteAllLines scrive ogni stringa seguita da un a capo; ReadAllLines restituisce uno string[]:
Output:
4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00
La differenza tra i due metodi di lettura conta per le dimensioni. ReadAllLines legge tutto il file in memoria prima di restituire. ReadLines restituisce un IEnumerable<string> lazy che legge man mano che iteri, quindi un foreach su un log da 5 GB usa pochi kilobyte. Con LINQ, ReadLines(...).Where(...).Take(10) smette di leggere appena ha dieci corrispondenze.
Analizzare un CSV con Split(',') funziona per file semplici prodotti da te. I CSV reali hanno campi tra virgolette che contengono virgole; per quelli usa una libreria come CsvHelper.
Nota la InvariantCulture in decimal.Parse: senza, analizzare "4.50" su una macchina impostata in tedesco o portoghese legge il punto come separatore delle migliaia.
StreamWriter e StreamReader
I metodi di File aprono e chiudono il file a ogni chiamata. Per scrivere molti pezzi nel tempo, o per leggere senza LINQ un file troppo grande per la memoria, apri uno stream una volta sola. Racchiudilo sempre in using così viene chiuso anche se viene lanciata un'eccezione; un writer non chiuso potrebbe non svuotare mai il suo buffer, e il file resta vuoto o troncato.
Output:
2: WARN disk 85% full
4: ERROR connection refused
ReadLine restituisce null alla fine del file, ed è questo che termina il ciclo while. Uno StreamWriter mette l'output in un buffer e lo scrive su disco quando il buffer si riempie, quando chiami Flush() e quando viene rilasciato; il blocco using garantisce l'ultimo caso.
Da C# 8, una dichiarazione using rilascia lo stream alla fine del blocco che la contiene, senza le graffe in più:
using var writer = new StreamWriter(path); // C# 8: disposed when the method returns
writer.WriteLine("INFO server started");
Percorsi e cartelle
Costruisci i percorsi con Path.Combine invece che concatenando stringhe. Inserisce il separatore giusto per il sistema operativo (\ su Windows, / su Linux e macOS) e non lo raddoppia. La classe Path scompone anche i percorsi, e Directory crea ed elenca le cartelle:
Output:
february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False
Una trappola: se un argomento successivo di Path.Combine è assoluto (inizia con / o \, o con una lettera di unità su Windows), tutto ciò che lo precede viene scartato, quindi Path.Combine("reports", "/2026") dà /2026. Passa parti relative, oppure usa Path.Join (.NET Core 3.0 e successivi), che non scarta mai niente.
Directory.GetFiles restituisce i file nell'ordine in cui li fornisce il file system, che cambia tra Windows e Linux, quindi ordina il risultato quando l'ordine conta. Directory.EnumerateFiles è la versione lazy, lo stesso rapporto che c'è tra ReadLines e ReadAllLines. Passa SearchOption.AllDirectories per includere le sottocartelle.
Gestire gli errori
Le operazioni sui file falliscono per motivi esterni al programma: il file manca, la cartella non esiste, un altro processo lo tiene aperto, il disco è pieno, i permessi sono sbagliati. Ognuno ha il suo tipo di eccezione. FileNotFoundException e DirectoryNotFoundException derivano da IOException, quindi catturale prima di quella; UnauthorizedAccessException invece no, quindi le serve un catch a parte:
Output:
missing file, using defaults
missing folder, using defaults
theme=dark
File.Exists prima di una lettura sembra più semplice, ma non elimina la necessità del try: il file può sparire, o essere bloccato da un altro programma, nell'istante tra il controllo e la lettura. Usa Exists per decidere cosa fare, e catch per sopravvivere a quello che succede davvero.
Codifica
File.WriteAllText, WriteAllLines e StreamWriter scrivono di default in UTF-8 senza byte order mark, e i metodi di lettura riconoscono UTF-8, UTF-16 e UTF-32 da un byte order mark, se presente. Passa un Encoding quando un file deve avere un formato preciso:
Output:
plain.txt 16 bytes, reads back unchanged: True
bom.txt 19 bytes, reads back unchanged: True
utf16.txt 30 bytes, reads back unchanged: True
é e ã occupano due byte ciascuno in UTF-8, ed è per questo che 14 caratteri richiedono 16 byte; tutti e tre i file vengono riletti correttamente perché il lettore riconosce il byte order mark. Encoding.UTF8 sembra il default ma aggiunge un byte order mark, che alcuni strumenti (vecchi importatori CSV, script di shell) mostrano come caratteri strani all'inizio della prima riga; usa new UTF8Encoding(false) per un UTF-8 esplicito senza BOM. Leggere un file con la codifica sbagliata non lancia eccezioni: produce caratteri sostitutivi, quindi scopri che codifica usa un file che non hai creato tu.
I/O asincrono sui file
Nelle app web e nel codice di interfaccia, bloccare un thread sull'I/O del disco lo spreca. .NET Core 2.0 e successivi hanno versioni asincrone dei metodi di File:
await File.WriteAllTextAsync("notes.txt", text);
string content = await File.ReadAllTextAsync("notes.txt");
string[] lines = await File.ReadAllLinesAsync("sales.csv");
StreamReader.ReadLineAsync e StreamWriter.WriteLineAsync funzionano allo stesso modo sugli stream.
Errori comuni
- Non rilasciare uno stream. Senza
using, il testo nel buffer potrebbe non arrivare mai su disco e il file resta bloccato finché non passa il garbage collector. - Costruire percorsi con
+ "\\" +. Non funziona su Linux e macOS. UsaPath.Combine. - Caricare file enormi con
ReadAllTextoReadAllLines. Leggili in streaming conReadLineso unoStreamReader. - Affidarsi alla cartella corrente. Dipende da come è stato avviato il programma. Usa percorsi assoluti o
AppContext.BaseDirectory. - Leggere numeri dai file con la cultura della macchina. Passa
CultureInfo.InvariantCultureper i file di dati. - Aspettarsi che
File.Existseviti le eccezioni. Il file può cambiare tra il controllo e l'uso.
Domande frequenti
Come scrivo del testo in un file in C#?
File.WriteAllText("notes.txt", text); crea il file, o lo sovrascrive se esiste, scrive la stringa in UTF-8 e lo chiude. Usa File.WriteAllLines(path, lines) per una collezione di righe e File.AppendAllText(path, text) per aggiungere in fondo invece di sostituire. Tutti e tre stanno in System.IO.
Come leggo un file di testo in C#?
string text = File.ReadAllText("notes.txt"); legge l'intero file in una sola stringa, e string[] lines = File.ReadAllLines(path); lo divide in righe. Per i file grandi usa File.ReadLines(path) in un foreach, che legge una riga alla volta invece di caricare tutto, oppure uno StreamReader.
Come aggiungo testo a un file in C#?
File.AppendAllText(path, text) aggiunge testo alla fine del file e crea il file se non esiste; File.AppendAllLines(path, lines) fa lo stesso con le righe. Con uno stream, aprilo in modalità append: new StreamWriter(path, append: true). Ricordati di aggiungere tu Environment.NewLine o \n quando accodi testo.
Come leggo un file riga per riga in C#?
foreach (string line in File.ReadLines(path)) { ... } legge il file una riga alla volta, quindi l'uso di memoria resta costante anche per log da diversi gigabyte. L'equivalente più vecchio è uno StreamReader in un blocco using con while ((line = reader.ReadLine()) != null). Evita ReadAllLines per i file grandi: carica prima tutte le righe in un array.
Come controllo se un file esiste in C#?
File.Exists(path) restituisce true se il file esiste e il programma può vederlo, e Directory.Exists(path) fa lo stesso per le cartelle. Non contarci per evitare le eccezioni: il file può essere cancellato o bloccato tra il controllo e la lettura, quindi gestisci comunque FileNotFoundException e IOException attorno all'operazione vera e propria sul file.