O namespace System.IO tem tudo para trabalhar com arquivos. Na maioria das tarefas, uma chamada na classe static File basta: ela abre o arquivo, lê ou escreve e o fecha. Para arquivos grandes ou controle fino, use StreamReader e StreamWriter. Acrescente using System.IO; a todos os exemplos abaixo.
Escrever e ler um arquivo inteiro
File.WriteAllText cria um arquivo (ou sobrescreve um existente) com o texto que você passa. File.ReadAllText retorna o arquivo inteiro como uma string. File.AppendAllText acrescenta no fim:
Saída:
True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False
Um caminho relativo como "notes.txt" é resolvido a partir do diretório atual do processo, que nem sempre é a pasta onde o seu programa está (um serviço, uma tarefa agendada ou um executor de testes pode começar em outro lugar). Para arquivos que acompanham a sua aplicação, monte o caminho a partir de AppContext.BaseDirectory.
WriteAllText sobrescreve sem perguntar. Se perder o conteúdo antigo importa, verifique File.Exists antes ou escreva em um arquivo temporário e depois o coloque no lugar com File.Move.
Linhas: WriteAllLines, ReadAllLines e ReadLines
Arquivos de registros, um por linha, são comuns o bastante para ter métodos próprios. WriteAllLines escreve cada string seguida de uma quebra de linha; ReadAllLines retorna um string[]:
Saída:
4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00
A diferença entre os dois leitores importa para o tamanho. ReadAllLines lê o arquivo inteiro na memória antes de retornar. ReadLines retorna um IEnumerable<string> preguiçoso que lê à medida que você itera, então um foreach sobre um log de 5 GB usa alguns kilobytes. Com LINQ, ReadLines(...).Where(...).Take(10) para de ler depois de achar dez correspondências.
Interpretar CSV com Split(',') funciona para arquivos simples que você mesmo produziu. CSV do mundo real tem campos entre aspas contendo vírgulas; use uma biblioteca como a CsvHelper nesses casos.
Repare no InvariantCulture em decimal.Parse: sem ele, converter "4.50" em uma máquina configurada em alemão ou português lê o ponto como separador de milhar.
StreamWriter e StreamReader
Os métodos de File abrem e fecham o arquivo a cada chamada. Para escrever muitos pedaços ao longo do tempo, ou para ler um arquivo grande demais para a memória sem LINQ, abra um stream uma vez. Sempre o envolva em using para que ele seja fechado mesmo se uma exceção for lançada; um writer não fechado pode nunca descarregar o buffer, e o arquivo acaba vazio ou cortado.
Saída:
2: WARN disk 85% full
4: ERROR connection refused
ReadLine retorna null no fim do arquivo, e é isso que encerra o laço while. Um StreamWriter guarda a saída em buffer e a grava em disco quando o buffer enche, quando você chama Flush() e quando ele é descartado; o bloco using garante o último caso.
Desde o C# 8, uma declaração using descarta o stream no fim do bloco que a contém, sem as chaves extras:
using var writer = new StreamWriter(path); // C# 8: disposed when the method returns
writer.WriteLine("INFO server started");
Caminhos e diretórios
Monte caminhos com Path.Combine em vez de concatenar strings. Ele insere o separador certo para o sistema operacional (\ no Windows, / no Linux e no macOS) e não o duplica. A classe Path também desmonta caminhos, e Directory cria e lista pastas:
Saída:
february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False
Uma armadilha: se um argumento posterior de Path.Combine tiver raiz (começar com / ou \, ou com uma letra de unidade no Windows), tudo o que vem antes é descartado, então Path.Combine("reports", "/2026") é /2026. Passe partes relativas, ou use Path.Join (.NET Core 3.0 em diante), que nunca descarta nada.
Directory.GetFiles retorna os arquivos na ordem que o sistema de arquivos der, que é diferente entre Windows e Linux, então ordene o resultado quando a ordem importar. Directory.EnumerateFiles é a versão preguiçosa, a mesma relação que existe entre ReadLines e ReadAllLines. Passe SearchOption.AllDirectories para incluir as subpastas.
Tratando erros
Operações com arquivos falham por motivos fora do seu programa: o arquivo não existe, a pasta não existe, outro processo está com ele aberto, o disco está cheio, as permissões estão erradas. Cada um tem o seu tipo de exceção. FileNotFoundException e DirectoryNotFoundException derivam de IOException, então capture-as antes dela; UnauthorizedAccessException não deriva, então precisa de um catch próprio:
Saída:
missing file, using defaults
missing folder, using defaults
theme=dark
File.Exists antes de uma leitura parece mais simples, mas não dispensa o try: o arquivo pode sumir, ou ser bloqueado por outro programa, no instante entre a verificação e a leitura. Use Exists para decidir o que fazer, e o catch para sobreviver ao que de fato acontece.
Codificação
File.WriteAllText, WriteAllLines e StreamWriter escrevem UTF-8 sem byte order mark por padrão, e os leitores detectam UTF-8, UTF-16 e UTF-32 a partir de um byte order mark, se houver um. Passe um Encoding quando um arquivo precisar de um formato específico:
Saída:
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 ã ocupam dois bytes cada em UTF-8, e é por isso que 14 caracteres precisam de 16 bytes, e os três arquivos são lidos de volta corretamente porque o leitor reconhece o byte order mark. Encoding.UTF8 parece o padrão, mas acrescenta um byte order mark, que algumas ferramentas (importadores de CSV antigos, scripts de shell) mostram como lixo no começo da primeira linha; use new UTF8Encoding(false) para UTF-8 explícito sem ele. Ler um arquivo na codificação errada não lança exceção: produz caracteres de substituição, então descubra qual codificação usa um arquivo que você não criou.
I/O de arquivos async
Em apps web e em código de interface, bloquear uma thread com I/O de disco é desperdício. O .NET Core 2.0 em diante tem versões async dos métodos de 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 funcionam do mesmo jeito em streams.
Erros comuns
- Não descartar um stream. Sem
using, o texto em buffer pode nunca chegar ao disco e o arquivo fica bloqueado até o garbage collector executar. - Montar caminhos com
+ "\\" +. Isso quebra no Linux e no macOS. UsePath.Combine. - Carregar arquivos enormes com
ReadAllTextouReadAllLines. Leia-os aos poucos comReadLinesou umStreamReader. - Depender do diretório atual. Ele depende de como o programa foi iniciado. Use caminhos absolutos ou
AppContext.BaseDirectory. - Converter números de arquivos com a cultura da máquina. Passe
CultureInfo.InvariantCulturepara arquivos de dados. - Esperar que
File.Existsevite exceções. O arquivo pode mudar entre a verificação e o uso.
Perguntas frequentes
Como escrever texto em um arquivo em C#?
File.WriteAllText("notes.txt", text); cria o arquivo, ou o sobrescreve se ele existir, grava a string como UTF-8 e o fecha. Use File.WriteAllLines(path, lines) para uma coleção de linhas e File.AppendAllText(path, text) para acrescentar no fim em vez de substituir. Os três ficam em System.IO.
Como ler um arquivo de texto em C#?
string text = File.ReadAllText("notes.txt"); lê o arquivo inteiro em uma string, e string[] lines = File.ReadAllLines(path); o divide em linhas. Para arquivos grandes, use File.ReadLines(path) em um foreach, que lê uma linha por vez em vez de carregar tudo, ou um StreamReader.
Como acrescentar texto a um arquivo em C#?
File.AppendAllText(path, text) acrescenta texto no fim do arquivo e cria o arquivo se ele não existir; File.AppendAllLines(path, lines) faz o mesmo com linhas. Com um stream, abra-o em modo de acréscimo: new StreamWriter(path, append: true). Lembre de acrescentar você mesmo Environment.NewLine ou \n ao acrescentar texto.
Como ler um arquivo linha por linha em C#?
foreach (string line in File.ReadLines(path)) { ... } lê o arquivo uma linha por vez, então o uso de memória fica estável mesmo para logs de gigabytes. O equivalente mais antigo é um StreamReader em um bloco using com while ((line = reader.ReadLine()) != null). Evite ReadAllLines para arquivos grandes: ele carrega todas as linhas em um array antes.
Como verificar se um arquivo existe em C#?
File.Exists(path) retorna true se o arquivo existe e o programa pode vê-lo, e Directory.Exists(path) faz o mesmo para pastas. Não conte com ele para evitar exceções: o arquivo pode ser apagado ou bloqueado entre a verificação e a leitura, então ainda trate FileNotFoundException e IOException em volta da operação real no arquivo.