Lendo arquivos em C#

Ler arquivos em C# não é uma tarefa difícil, e o .NET oferece caminhos diretos para fazer isso com o menor esforço possível. Neste guia, você conhece as principais formas de leitura, entende quando usar cada uma e aprende a evitar problemas de memória, codificação e recursos abertos.

A forma mais simples de ler arquivos em C#

A forma mais simples e direta de ler arquivos em C# é usar o método ReadAllText da classe estática File. Ele abre o arquivo, lê todo o conteúdo para uma única string e fecha o arquivo em seguida.

No exemplo abaixo, temos um programa completo que faz a leitura de um arquivo e exibe o conteúdo na tela com o Console.WriteLine.

// Lê um arquivo inteiro
var data = File.ReadAllText("C:\\text-file.txt");
Console.WriteLine(data);

Repare que não existe nenhum laço de repetição nem objeto para descartar. Por isso, essa abordagem funciona muito bem em scripts, ferramentas de linha de comando e arquivos de configuração pequenos.

Segundo a documentação oficial do File.ReadAllText, o método garante o fechamento do handle do arquivo, mesmo quando ocorre uma exceção. Assim, você não precisa se preocupar em liberar o recurso manualmente.

Como o ReadAllText detecta a codificação

Além de ler o conteúdo, o ReadAllText tenta detectar a codificação do arquivo pela presença do BOM (byte order mark). Dessa forma, ele reconhece automaticamente UTF-8, UTF-16 e UTF-32 quando o arquivo começa com a marca correspondente.

Quando o arquivo vem de um sistema legado, porém, a detecção automática pode falhar. Nesse caso, informe a codificação de forma explícita no segundo parâmetro.

using System.Text;

// Lê o arquivo usando uma codificação específica
var data = File.ReadAllText("C:\\text-file.txt", Encoding.UTF8);
Console.WriteLine(data);

Na prática, acentos trocados por símbolos estranhos quase sempre indicam uma codificação errada. Então, antes de ler arquivos em C# vindos de outros sistemas, confirme qual codificação eles usam.

Lendo arquivos em C# linha a linha

Como notamos no método anterior, não iteramos sobre o arquivo, apenas pegamos todo o conteúdo e exibimos na tela. Contudo, muitos cenários exigem tratar cada linha de forma separada, como em logs ou listas de registros.

Para iterar pelas linhas do arquivo, temos o método ReadAllLines, que retorna um array de string. Com isso, você pode ler arquivos em C# e percorrer o resultado com um foreach comum.

No exemplo abaixo, temos um programa completo que faz a leitura de um arquivo linha a linha e exibe cada linha no console.

// Lê um arquivo linha a linha
var lines = File.ReadAllLines("C:\\text-file.txt");
foreach (var line in lines)
    Console.WriteLine(line);

O ReadAllLines considera como linha qualquer sequência de caracteres seguida de \r, \n ou \r\n. Além disso, as strings retornadas não incluem esses caracteres de quebra.

Outro detalhe útil: se o arquivo termina com uma quebra de linha, o método não acrescenta uma linha vazia ao final do array. Ou seja, um arquivo com "line1\nline2\n" gera exatamente dois elementos.

O custo de carregar tudo na memória

Tanto o ReadAllText quanto o ReadAllLines carregam o arquivo inteiro na memória antes de devolver o controle. Para arquivos de poucos kilobytes ou megabytes, isso não faz diferença.

No entanto, um arquivo de vários gigabytes vira um problema sério. Nesse caso, ler o arquivo por completo pode acarretar estouro de memória e derrubar a aplicação.

Portanto, a forma de ler arquivos em C# depende do tamanho esperado de cada arquivo. Quando você não controla esse tamanho, prefira as abordagens baseadas em streaming que veremos a seguir.

File.ReadLines e a leitura sob demanda

Entre o ReadAllLines e o StreamReader, existe um meio-termo pouco conhecido: o File.ReadLines. Em vez de um array, ele retorna um IEnumerable<string> que lê as linhas conforme o código avança na iteração.

// Lê as linhas sob demanda, sem carregar o arquivo inteiro
foreach (var line in File.ReadLines("C:\\text-file.txt"))
{
    if (line.Contains("ERROR"))
        Console.WriteLine(line);
}

A documentação explica a diferença com clareza. Com o ReadLines, você começa a enumerar a coleção antes de ela estar completa, enquanto o ReadAllLines obriga você a esperar o array inteiro.

Por isso, para ler arquivos em C# muito grandes, o ReadLines costuma ser mais eficiente. Além disso, ele combina muito bem com consultas LINQ, já que a filtragem acontece durante a leitura.

// Conta quantas linhas de erro existem no log
var errorCount = File.ReadLines("C:\\app.log")
    .Count(line => line.StartsWith("ERROR"));

Console.WriteLine($"Errors found: {errorCount}");

Nesse exemplo, apenas uma linha fica na memória por vez. Assim, você consegue ler arquivos em C# de vários gigabytes sem pressionar o garbage collector.

StreamReader para ler arquivos em C# grandes

Uma outra forma muito solicitada é a leitura de arquivos linha a linha com controle total do processo. Para isso, uma ótima solução é usar o StreamReader em conjunto com o using, que faz a leitura do arquivo em blocos (streaming).

O StreamReader herda de TextReader e foi projetado para ler caracteres de um fluxo de bytes em uma codificação específica. Na prática, ele mantém um buffer interno e entrega o texto aos poucos.

No exemplo abaixo, temos um programa completo que faz a leitura de um arquivo e exibe o conteúdo na tela com o Console.WriteLine.

using var file = new StreamReader("C:\\text-file.txt");
string? line;

while ((line = file.ReadLine()) != null)
    Console.WriteLine(line);

// Opcional: o using já fecha o arquivo ao final do escopo
file.Close();

Ao ler arquivos em C# com o StreamReader, o método ReadLine lê uma linha e avança o ponteiro para a próxima. Quando chega ao fim do arquivo, ele retorna null, e é exatamente essa condição que encerra o laço while.

Por que o using importa

Toda vez que trabalhamos com um stream, existe a possibilidade de o arquivo ficar aberto por mais tempo que o necessário. Por isso, o uso do Dispose é quase obrigatório, e o using faz essa chamada por você.

A documentação do StreamReader reforça esse ponto, pois o tipo implementa a interface IDisposable. Então, quando terminar de usar o objeto, descarte-o diretamente com Dispose ou indiretamente com o using.

A declaração using var encerra o recurso no fim do escopo atual, mesmo quando ocorre uma exceção. Dessa forma, a chamada a file.Close() no exemplo acima se torna redundante, embora não cause nenhum problema.

Se você quiser entender melhor o que acontece com os objetos depois do descarte, vale ler o artigo sobre o garbage collector em C#. Afinal, liberar o handle do arquivo e liberar a memória são responsabilidades diferentes.

Codificação padrão do StreamReader

O StreamReader usa UTF-8 por padrão, em vez da página de código ANSI do sistema operacional. Com isso, ele trata caracteres Unicode corretamente e gera resultados consistentes em versões localizadas do sistema.

Vale um alerta: a propriedade CurrentEncoding só fica confiável depois da primeira chamada a um método Read. Isso acontece porque a detecção automática da codificação ocorre apenas nesse momento.

Outro ponto importante é que o StreamReader não é thread safe por padrão. Portanto, não compartilhe a mesma instância entre várias threads sem sincronização.

Leitura assíncrona de arquivos

Em aplicações web e desktop, ler arquivos em C# de forma síncrona bloqueia uma thread esperando o disco e desperdiça recursos. Por esse motivo, a classe File também oferece versões assíncronas, como ReadAllTextAsync e ReadAllLinesAsync.

// Lê o arquivo inteiro sem bloquear a thread atual
var content = await File.ReadAllTextAsync("C:\\text-file.txt");
Console.WriteLine(content);

O StreamReader segue o mesmo caminho com o método ReadLineAsync. Assim, você combina o consumo baixo de memória do streaming com a liberação da thread durante a espera.

using var reader = new StreamReader("C:\\text-file.txt");
string? line;

// Lê uma linha por vez de forma assíncrona
while ((line = await reader.ReadLineAsync()) != null)
    Console.WriteLine(line);

A partir do .NET 7, existe também o File.ReadLinesAsync, que retorna um IAsyncEnumerable<string>. Com ele, a leitura sob demanda passa a funcionar com await foreach.

// Itera as linhas de forma assíncrona e sob demanda
await foreach (var line in File.ReadLinesAsync("C:\\text-file.txt"))
    Console.WriteLine(line);

Em APIs ASP.NET Core, essa combinação ajuda a manter o servidor responsivo. Para ir além, confira como fazer streaming assíncrono no ASP.NET.

Tratamento de erros ao ler arquivos em C#

Ao ler arquivos em C#, você lida com um recurso externo que pode falhar a qualquer momento. O arquivo pode não existir, estar bloqueado por outro processo ou exigir permissões que o usuário não tem.

Os métodos da classe File lançam exceções específicas para cada situação. Entre as mais comuns estão FileNotFoundException, DirectoryNotFoundException, UnauthorizedAccessException e IOException.

try
{
    var content = File.ReadAllText("C:\\text-file.txt");
    Console.WriteLine(content);
}
catch (FileNotFoundException)
{
    Console.WriteLine("File not found.");
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("Access denied.");
}
catch (IOException ex)
{
    Console.WriteLine($"The file could not be read: {ex.Message}");
}

Repare na ordem dos blocos catch. Como FileNotFoundException herda de IOException, o tratamento mais específico precisa vir antes do mais genérico.

Verificar a existência não basta

Muita gente chama File.Exists antes da leitura e considera o problema resolvido. Contudo, entre a verificação e a abertura, outro processo pode apagar ou bloquear o arquivo.

Por isso, use o File.Exists para decidir fluxos de negócio, mas mantenha o try/catch em volta da leitura. Em outras palavras, a exceção continua sendo a garantia real.

Caminhos de arquivo portáveis

Todos os exemplos até aqui usam "C:\\text-file.txt", um caminho típico do Windows. Note que a barra invertida aparece duplicada, porque em C# ela inicia uma sequência de escape dentro da string.

Uma alternativa é usar uma string verbatim, prefixada com @. Nesse formato, a barra invertida perde o significado especial e o caminho fica mais legível.

// String verbatim: a barra invertida não precisa de escape
var data = File.ReadAllText(@"C:\text-file.txt");

Como o .NET roda em Windows, Linux e macOS, caminhos fixos limitam a portabilidade de quem precisa ler arquivos em C# em vários sistemas. Então, prefira montar o caminho com Path.Combine, que usa o separador correto para cada sistema.

// Monta o caminho a partir da pasta da aplicação
var path = Path.Combine(AppContext.BaseDirectory, "data", "text-file.txt");
var data = File.ReadAllText(path);

Caso precise também criar, copiar ou excluir arquivos, o guia de manipulação de arquivos em C# aprofunda essas operações.

Ler arquivos em C# binários com ReadAllBytes

Nem todo arquivo contém texto. Imagens, PDFs e arquivos compactados exigem leitura de bytes, e não de caracteres.

Para esse cenário, a classe File oferece o método ReadAllBytes, que retorna um array de byte com todo o conteúdo. Assim como o ReadAllText, ele abre, lê e fecha o arquivo em uma única chamada.

// Lê todos os bytes de uma imagem
var bytes = File.ReadAllBytes("C:\\logo.png");
Console.WriteLine($"Size: {bytes.Length} bytes");

Esse método também carrega tudo na memória. Portanto, ao ler arquivos em C# binários de tamanho grande, prefira abrir um FileStream e processar o conteúdo em blocos.

using var stream = File.OpenRead("C:\\backup.zip");
var buffer = new byte[81920];
int bytesRead;

// Processa o arquivo em blocos de 80 KB
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
    Console.WriteLine($"Block read: {bytesRead} bytes");

Nesse caso, a memória usada fica limitada ao tamanho do buffer, independentemente do tamanho do arquivo. Além disso, o mesmo padrão serve para enviar o conteúdo para a rede ou para outro stream.

Qual método usar para ler arquivos em C#

Depois de conhecer as opções, a escolha fica mais simples. A tabela abaixo resume quando cada abordagem faz sentido.

Método Retorno Memória Melhor uso
File.ReadAllText string Arquivo inteiro Arquivos pequenos e configurações
File.ReadAllLines string[] Arquivo inteiro Arquivos pequenos com acesso por índice
File.ReadLines IEnumerable<string> Uma linha por vez Arquivos grandes e consultas LINQ
StreamReader Linhas ou blocos Buffer interno Controle fino, codificação e streams
Versões Async Task ou IAsyncEnumerable Varia Aplicações web e interfaces gráficas

Em resumo, comece pelo ReadAllText quando o arquivo é pequeno e previsível. Quando o tamanho cresce ou é desconhecido, migre para ReadLines ou StreamReader.

Além disso, se o arquivo tem formato estruturado, considere uma biblioteca específica. Para planilhas simples, por exemplo, o artigo sobre geração de CSV em C# mostra o caminho inverso, a escrita.

Boas práticas ao ler arquivos em C#

Algumas práticas simples evitam a maior parte dos problemas ao ler arquivos em C# em produção. Primeiro, sempre descarte streams com using, mesmo em códigos curtos.

Em seguida, defina a codificação de forma explícita quando o arquivo vier de fora da sua aplicação. Assim, você evita surpresas com acentuação em nomes, endereços e descrições.

Também evite carregar arquivos de tamanho desconhecido com ReadAllText. Um upload inesperado de vários gigabytes pode consumir toda a memória do servidor.

Outro cuidado é tratar as exceções de entrada e saída perto do ponto de leitura. Dessa forma, a mensagem de erro fica clara e o restante do sistema não precisa conhecer detalhes de disco.

Vale ainda validar o caminho recebido quando ele vem do usuário. Um caminho montado sem cuidado pode apontar para pastas sensíveis do servidor e expor arquivos que nunca deveriam sair dali.

Por fim, prefira as versões assíncronas em aplicações que atendem muitos usuários ao mesmo tempo. Enquanto o disco responde, a thread fica livre para processar outras requisições, e a aplicação continua respondendo bem mesmo sob carga.

Conclusão

C# não é difícil, e muitas vezes nós complicamos coisas que podem, e devem, ser simples. Ler arquivos é uma tarefa comum em todas as linguagens, e o C# torna isso simples e direto ao ponto.

Para a maioria dos casos, o File.ReadAllText resolve em uma linha. Quando o arquivo cresce, o File.ReadLines e o StreamReader garantem leitura sob demanda sem estourar a memória.

Com using, tratamento de exceções e codificação explícita, você passa a ler arquivos em C# com segurança em qualquer tipo de aplicação. Agora, escolha o método certo para o seu cenário e coloque esses exemplos em prática.

FAQ

Qual a forma mais simples de ler um arquivo em C#?

A forma mais simples é usar File.ReadAllText, que lê todo o conteúdo para uma string e fecha o arquivo automaticamente. Ela funciona melhor com arquivos pequenos.

Qual a diferença entre ReadAllLines e ReadLines?

O ReadAllLines carrega todas as linhas em um array antes de devolver o resultado. Já o ReadLines entrega as linhas sob demanda, o que economiza memória em arquivos grandes.

Preciso chamar Close depois de usar o StreamReader com using?

Não, porque o using chama o Dispose ao final do escopo e fecha o arquivo. A chamada a Close fica redundante, embora não cause erro.

Como evitar problemas com acentos ao ler um arquivo?

Informe a codificação correta, como Encoding.UTF8, no método de leitura ou no construtor do StreamReader. Assim, os caracteres especiais aparecem corretamente.

Existe leitura assíncrona de arquivos no .NET?

Sim, a classe File oferece métodos como ReadAllTextAsync e, a partir do .NET 7, ReadLinesAsync. O StreamReader também possui o ReadLineAsync.