Vamos ver treze formas de ler um arquivo de texto no .NET, e duas delas cobrem quase todos os casos. File.ReadAllText() lê o arquivo inteiro para uma única string. File.ReadLines() lê o arquivo de forma incremental, uma linha por vez, então o uso de memória fica estável, seja qual for o tamanho do arquivo.
Todo o resto é variação. StreamReader oferece as mesmas leituras em um nível mais baixo quando já temos um stream; ReadBlock() lê blocos de tamanho fixo; BufferedStream adiciona um segundo buffer por cima do que FileStream já tem; e toda leitura síncrona tem uma equivalente assíncrona.
Qual é o mais rápido depende do arquivo. Mais abaixo, medimos o desempenho dos treze com um arquivo de cerca de 98 KB, em que File.ReadAllText() e StreamReader.ReadToEnd() terminam empatados. Em um arquivo que não cabe na memória, a pergunta muda de forma, e a resposta também.
Preparação do arquivo de origem
Antes de discutirmos os diferentes métodos de acesso ao arquivo, vamos criar um arquivo de origem que possamos usar em todos os nossos casos.
Primeiro, vamos adicionar uma string a uma classe de teste do projeto:
private const string ExpectedText
= """
Integer facilisis ex libero, ut suscipit leo blandit non. Vivamus nec ipsum orci.
Proin nec mauris dui. Proin at felis et eros commodo aliquet.
Pellentesque lacinia porta leo, non accumsan turpis sagittis et.
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere.
""";
Vamos usar essa string para criar um arquivo temporário na classe de teste:
private static readonly string TempFilePath = Path.GetTempFileName();
O método GetTempFileName() da classe Path cria um arquivo temporário para nós na pasta temporária. Depois, retorna o caminho completo do arquivo. Se preferirmos não passar caminho absoluto nenhum aos métodos, há mais de uma forma de ler um arquivo de texto sem especificar o caminho completo.
No construtor da classe de teste, gravamos a string nesse arquivo com File.WriteAllText(TempFilePath, ExpectedText). Esse arquivo vai servir como arquivo de texto de exemplo para os métodos de teste.
Uma suposição atravessa todo o exemplo: o arquivo não está vazio. Seis dos métodos abaixo terminam subtraindo Environment.NewLine.Length do comprimento de um StringBuilder para remover a quebra de linha final e, em um arquivo de zero bytes, esse objeto fica vazio e a subtração lança uma ArgumentOutOfRangeException. Tratar esse caso em seis lugares esconderia o objetivo de cada exemplo, então os exemplos continuam enxutos, do tamanho certo para ensinar, e registramos a suposição aqui.
Ler um arquivo de texto com o método ReadAllLines
Com toda a configuração pronta, vamos ver o primeiro método para ler um arquivo de texto. Como todos os métodos abaixo, ele fica em uma classe WaysToReadATextFileInCsharp cujo construtor recebe o caminho do arquivo e o guarda em um campo _sampleFilePath, com using System.Buffers; e using System.Text; no início do arquivo:
public string UseFileReadAllLines()
{
var stringBuilder = new StringBuilder();
foreach (var line in File.ReadAllLines(_sampleFilePath))
{
stringBuilder.AppendLine(line);
}
stringBuilder.Length -= Environment.NewLine.Length;
return stringBuilder.ToString();
}
Aqui, chamamos o método File.ReadAllLines() para ler todas as linhas do arquivo de texto para a memória. Esse método lê todas as linhas do arquivo e as converte em elementos de um array de strings. O método retorna esse array de strings depois de ler o arquivo inteiro.
Depois, criamos um objeto StringBuilder que vai conter o texto do arquivo.
Usamos o método StringBuilder.AppendLine() para acrescentar as linhas à string final. Sempre que usamos esse método, a string resultante pode ser diferente do conteúdo original do arquivo.
Essas diferenças podem surgir porque, por padrão, o método AppendLine() usa o caractere de nova linha definido por Environment.NewLine. Se, por exemplo, o arquivo usa \r como caractere de final de linha e o sistema atual adota \r\n, a string retornada pode não representar com exatidão o texto inicial.
Depois disso, removemos o último caractere de nova linha subtraindo o valor da propriedade Environment.NewLine.Length do comprimento do stringBuilder.
Por fim, montamos e retornamos a string chamando o método ToString().
Como ler um arquivo de texto com o método ReadAllText
Em seguida, temos o método ReadAllText():
public string UseFileReadAllText() => File.ReadAllText(_sampleFilePath);
Neste método, chamamos o método File.ReadAllText() com o caminho do arquivo de exemplo como argumento. Com esse método conciso, lemos diretamente todas as linhas do arquivo e as retornamos como uma única string.
Como ler um arquivo de texto linha por linha em C#?
File.ReadLines() lê um arquivo de texto linha por linha sem carregar o arquivo inteiro na memória. Ele retorna um IEnumerable<string>, e cada passo do loop tira a próxima linha do stream.
Toda a diferença entre ele e File.ReadAllLines() está nessa leitura sob demanda. ReadAllLines() lê o arquivo inteiro primeiro e retorna um string[], então um log de dois gigabytes vai parar na memória de uma vez só, e ocupa ainda mais: as strings do .NET são UTF-16, então um arquivo ASCII praticamente dobra de tamanho ao ser carregado. ReadLines() nunca guarda mais do que a linha atual.
StreamReader.ReadLine() faz o mesmo trabalho um nível abaixo. Nós mesmos abrimos o leitor e chamamos ReadLine() até que ele retorne null, que é o que queremos quando o loop precisa parar antes, pular adiante ou manter uma contagem acumulada.
Para um arquivo pequeno, os três são intercambiáveis e a escolha é questão de gosto. Para um arquivo que não cabe na memória, só os dois que leem sob demanda são opções viáveis.
Para começar, vamos definir um método UseFileReadLines():
public string UseFileReadLines()
{
var stringBuilder = new StringBuilder();
foreach (var line in File.ReadLines(_sampleFilePath))
{
stringBuilder.AppendLine(line);
}
stringBuilder.Length -= Environment.NewLine.Length;
return stringBuilder.ToString();
}
Aqui, usamos o método File.ReadLines() para percorrer o arquivo de texto linha por linha.
Esse método lê as linhas do arquivo e retorna uma instância de IEnumerable<string> que podemos usar para iterar sobre o arquivo. Ao contrário do método File.ReadAllLines(), ele não carrega o arquivo inteiro na memória. Em vez disso, lê o arquivo linha por linha, sob demanda.
Assim, durante a iteração sobre o IEnumerable, cada passo nos entrega a próxima linha. O leitor continua preenchendo um buffer interno, então não há uma leitura de disco por linha; o que se mantém constante é quanto do arquivo estamos guardando, e não quantas vezes acessamos o disco. Depois do loop, removemos o último caractere de nova linha do stringBuilder.
Por último, montamos e retornamos a string chamando o método ToString().
Ler linha por linha também é o formato da maior parte do trabalho que fazemos com linhas, desde filtrar registros até contar as linhas de um arquivo de texto sem guardar nada dele na memória.
Ler um arquivo de texto com o método StreamReader.ReadLine
Agora, vamos falar do método StreamReader.ReadLine():
public string UseStreamReaderReadLine()
{
using var streamReader = new StreamReader(_sampleFilePath);
var stringBuilder = new StringBuilder();
while (streamReader.ReadLine() is { } fileLine)
{
stringBuilder.AppendLine(fileLine);
}
stringBuilder.Length -= Environment.NewLine.Length;
return stringBuilder.ToString();
}
Aqui, lemos as linhas do arquivo uma a uma usando um objeto StreamReader.
Começamos inicializando uma instância de StreamReader e passando para ela o caminho do arquivo de exemplo. Usamos a instrução using para garantir o descarte correto do StreamReader no fim da execução do método.

E-book gratuito
Sua Web API está pronta para produção?
33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.
O e-book está em inglês.
Baixe o checklist gratuitoPDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.
Em seguida, criamos um objeto StringBuilder que vamos usar no loop.
A ação principal acontece dentro do loop while. Na condição do loop, usamos correspondência de padrões (pattern matching) para ler as linhas do arquivo. À medida que lemos cada linha, nós a atribuímos à variável fileLine. Depois, adicionamos essa linha à instância stringBuilder com o método stringBuilder.AppendLine(). Isso continua até chegarmos ao fim do arquivo e o StreamReader retornar um valor null.
De novo, depois de terminar a leitura do arquivo, garantimos a remoção do último caractere de nova linha no fim do stringBuilder. Depois, montamos e retornamos a string chamando o método ToString().
Ler um arquivo de texto com o método ReadToEnd
Seguindo em frente, temos o método ReadToEnd():
public string UseStreamReaderReadToEnd()
{
using var streamReader = new StreamReader(_sampleFilePath);
return streamReader.ReadToEnd();
}
Aqui, criamos uma instância de StreamReader com o arquivo de exemplo.
Depois, lemos todo o conteúdo do stream chamando o método StreamReader.ReadToEnd().
Com esse método, seguimos outro caminho e lemos todo o conteúdo do arquivo diretamente para a memória, de uma só vez.
Uso do método ReadBlock
Também podemos usar o método StreamReader.ReadBlock() para ler um arquivo de texto:
public string UseStreamReaderReadBlock()
{
using var streamReader = new StreamReader(_sampleFilePath);
var buffer = new char[4096];
int numberRead;
var stringBuilder = new StringBuilder();
while ((numberRead = streamReader.ReadBlock(buffer, 0, buffer.Length)) > 0)
{
stringBuilder.Append(buffer[..numberRead]);
}
return stringBuilder.ToString();
}
Inicialmente, criamos um StreamReader para o arquivo de destino. Depois, criamos um array de 4.096 caracteres, pois esse é o tamanho de buffer padrão que FileStream usa.
Também declaramos um inteiro, numberRead, para guardar o número de caracteres lidos em cada iteração do loop. Em seguida, criamos um objeto StringBuilder que vamos usar no loop.
Em cada iteração do loop, verificamos se o número de caracteres lidos continua positivo. Se continuar, isso significa que há mais conteúdo no stream. Então, lemos os dados e os adicionamos à instância de StringBuilder.
Dentro do loop, passamos ao método Append() os caracteres que vão do início do array de buffer até o caractere no índice numberRead - 1. Com isso, acrescentamos os caracteres que acabaram de ser lidos durante a operação do método ReadBlock(). Repetimos esse processo até que o valor de numberRead caia para zero ou menos, o que indica o fim do arquivo, e então saímos do loop.
Quando o loop while termina, montamos e retornamos a string chamando o método ToString().
Além disso, podemos usar a classe ArrayPool do namespace System.Buffers para definir o buffer:
public string UseStreamReaderReadBlockWithArrayPool()
{
using var streamReader = new StreamReader(_sampleFilePath);
var buffer = ArrayPool<char>.Shared.Rent(4096);
int numberRead;
var stringBuilder = new StringBuilder();
while ((numberRead = streamReader.ReadBlock(buffer, 0, buffer.Length)) > 0)
{
stringBuilder.Append(buffer[..numberRead]);
}
ArrayPool<char>.Shared.Return(buffer);
return stringBuilder.ToString();
}
Aqui, o buffer vem de uma instância compartilhada da classe ArrayPool<char>. Obtemos essa instância com a propriedade Shared da classe ArrayPool.
ArrayPool é uma classe do .NET que nos ajuda a criar e reutilizar instâncias de array com eficiência. Ao usar essa classe, reduzimos a demanda de memória da aplicação e melhoramos o desempenho geral dela. Explicamos com mais profundidade como o ArrayPool reduz alocações em um artigo próprio.
Depois, chamamos o método Rent() para obter um buffer com comprimento de 4.096. Quando terminamos de usar o buffer, nós o devolvemos ao ArrayPool chamando o método Return(). No final, montamos e retornamos a string chamando o método ToString().
Ler um arquivo de texto com o método ReadBlock e a estrutura Span
Agora, vamos ver outra forma de usar o método ReadBlock(), incorporando a estrutura Span:
public string UseStreamReaderReadBlockWithSpan()
{
using var streamReader = new StreamReader(_sampleFilePath);
var buffer = new char[4096].AsSpan();
int numberRead;
var stringBuilder = new StringBuilder();
while ((numberRead = streamReader.ReadBlock(buffer)) > 0)
{
stringBuilder.Append(buffer[..numberRead]);
}
return stringBuilder.ToString();
}
Neste método, convertemos o buffer em um Span<char> usando o método AsSpan().
Depois, na definição do loop while, simplificamos a chamada ao método ReadBlock() passando apenas o buffer, agora como um Span. Em seguida, percorremos o arquivo e acrescentamos os caracteres dele ao objeto stringBuilder.
Quando o loop termina, montamos e retornamos a string.
Como ler um arquivo de texto com um objeto BufferedStream
Agora, vamos ver como ler um arquivo de texto criando um objeto BufferedStream:
public string UseBufferedStreamObject()
{
var stringBuilder = new StringBuilder();
using var fileStream = new FileStream(_sampleFilePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read);
using var bufferedStream = new BufferedStream(fileStream);
using var streamReader = new StreamReader(bufferedStream);
while (streamReader.ReadLine() is { } fileLine)
{
stringBuilder.AppendLine(fileLine);
}
stringBuilder.Length -= Environment.NewLine.Length;
return stringBuilder.ToString();
}
Neste método, primeiro criamos um novo StringBuilder para reunir o texto. Depois, criamos uma instância aberta de FileStream a partir do arquivo de exemplo. Com esse FileStream, configuramos um BufferedStream. E, usando esse BufferedStream, criamos um StreamReader.
Agora vem a ação de verdade, o loop while. Enquanto continuamos obtendo linhas com o método StreamReader.ReadLine(), nós as adicionamos ao stringBuilder com o método AppendLine(). No momento em que fileLine se torna null, é sinal de que terminamos de ler do bufferedStream. Isso também significa que chegamos ao fim do arquivo e saímos do loop.
Quando terminamos tudo, removemos o último caractere de nova linha e montamos e retornamos a string chamando o método ToString().
Embora esse método faça a leitura corretamente, a classe FileStream usa por padrão um buffer interno de 4.096 bytes. Isso pode levar a um buffer duplo: o buffer do FileStream e o buffer do BufferedStream.
Ler um arquivo de texto com um objeto BufferedStream e o buffer do FileStream desativado
Por fim, vamos ver como resolver a situação de “buffer duplo”. Para isso, vamos desativar o buffer do FileStream:
public string UseBufferedStreamObjectWithNoFileStreamBuffer()
{
var stringBuilder = new StringBuilder();
using var fileStream = new FileStream(_sampleFilePath, new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
BufferSize = 0
});
using var bufferedStream = new BufferedStream(fileStream);
using var streamReader = new StreamReader(bufferedStream);
while (streamReader.ReadLine() is { } fileLine)
{
stringBuilder.AppendLine(fileLine);
}
stringBuilder.Length -= Environment.NewLine.Length;
return stringBuilder.ToString();
}
Definimos BufferSize como 0 para desativar o buffer, e fazemos isso pela sobrecarga FileStreamOptions, e não pelo construtor mais antigo, que recebe as opções como argumentos separados. Essa escolha é proposital. A referência do Microsoft Learn para FileStreamOptions.BufferSize diz que “0 ou 1 significa que o buffer deve ser desativado”, enquanto a sobrecarga do construtor de FileStream que recebe um argumento bufferSize documenta esse valor como maior que zero e lista uma ArgumentOutOfRangeException para zero. As duas formas rodam, mas só uma delas está documentada para fazer o que esta seção ensina. Depois disso, seguimos o mesmo processo e retornamos a string.
Como ler um arquivo de texto de forma assíncrona em C#?
Toda leitura síncrona desta página tem uma equivalente assíncrona e, dentro de uma aplicação web, a assíncrona é a escolha padrão correta. Uma leitura bloqueante prende uma thread do pool de threads durante toda a operação de disco, e essa thread poderia estar atendendo a uma requisição.
O porém é que só ganhamos isso se o arquivo tiver sido aberto com esse objetivo. Os métodos File.*Async abrem o arquivo dessa forma; um StreamReader criado a partir de um caminho, não, e o runtime então simula a assincronia no pool de threads.
File.ReadAllTextAsync() retorna uma Task<string> e é o substituto direto de File.ReadAllText(). File.ReadAllLinesAsync() retorna uma Task<string[]>.

E-book gratuito
Sua Web API está pronta para produção?
33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.
O e-book está em inglês.
Baixe o checklist gratuitoPDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.
File.ReadLinesAsync() é o que lê sob demanda. Ele chegou no .NET 7 e retorna um IAsyncEnumerable<string>, então o consumimos com await foreach e continuamos sem guardar mais de uma linha na memória.
Assíncrono não é o mesmo que mais rápido. No arquivo do nosso benchmark, as leituras assíncronas levam cerca do triplo do tempo das equivalentes bloqueantes. O que elas oferecem é uma thread livre, e não uma espera menor.
Vamos começar pelo substituto direto de File.ReadAllText():
public async Task<string> UseFileReadAllTextAsync()
=> await File.ReadAllTextAsync(_sampleFilePath);
Não há nada a preparar aqui: o método abre o arquivo com E/S assíncrona ativada, lê até o fim e retorna a string.
Agora, a versão que lê sob demanda:
public async Task<string> UseFileReadLinesAsync()
{
var stringBuilder = new StringBuilder();
await foreach (var line in File.ReadLinesAsync(_sampleFilePath))
{
stringBuilder.AppendLine(line);
}
stringBuilder.Length -= Environment.NewLine.Length;
return stringBuilder.ToString();
}
Este método espelha UseFileReadLines() linha a linha, com await foreach no lugar de foreach, para que o benchmark compare coisas equivalentes. Em código real, faríamos o trabalho dentro do loop e nunca chegaríamos a montar a string. É o mesmo padrão que usamos ao consumir um IAsyncEnumerable com yield.
Por fim, a versão com StreamReader, que é a que exige cuidado:
public async Task<string> UseStreamReaderReadToEndAsync()
{
using var fileStream = new FileStream(_sampleFilePath, new FileStreamOptions
{
Mode = FileMode.Open,
Access = FileAccess.Read,
Share = FileShare.Read,
Options = FileOptions.Asynchronous | FileOptions.SequentialScan
});
using var streamReader = new StreamReader(fileStream);
return await streamReader.ReadToEndAsync();
}
Nós mesmos criamos o FileStream para poder definir FileOptions.Asynchronous. Escrever new StreamReader(path) e depois aguardar ReadToEndAsync() compila e retorna o texto certo, mas o arquivo foi aberto sem esse indicador, então a leitura só é assíncrona do ponto de vista do nosso código: por baixo dos panos, uma thread do pool de threads faz a espera bloqueante que estávamos tentando evitar. ReadLineAsync() e ReadBlockAsync() espelham da mesma forma seus equivalentes bloqueantes e trazem a mesma ressalva.
Qual é a forma mais rápida de ler um arquivo de texto em C#?
No arquivo do nosso benchmark, dois métodos dividem o topo da tabela: File.ReadAllText() e StreamReader.ReadToEnd(). Os dois fazem uma única passagem pelo arquivo e retornam uma única string, alocam a mesma quantidade de memória e terminam com um ou dois microssegundos de diferença entre si, o que está dentro da variação entre execuções.
O que pedimos importa tanto quanto o método que chamamos. Oito dos dez métodos síncronos leem linhas ou blocos e depois remontam uma única string com um StringBuilder, então pagam pela remontagem além da leitura. ReadToEnd() e File.ReadAllText() fazem essa etapa dentro do runtime, direto do buffer do próprio StreamReader, e isso explica parte da vantagem deles.
O tamanho importa mais do que as duas coisas. O arquivo medido aqui tem cerca de 98 KB, pequeno o bastante para ficar no cache do sistema operacional e pequeno o bastante para que carregá-lo inteiro quase não custe nada. Se ele tiver um gigabyte, o método mais rápido passa a ser aquele que nunca o carrega inteiro.
| Método | Retorna | Guarda o arquivo inteiro na memória | Forma assíncrona | Quando usar |
|---|---|---|---|---|
File.ReadAllText() | string | Sim | File.ReadAllTextAsync() | Queremos o arquivo inteiro como uma única string |
File.ReadAllLines() | string[] | Sim | File.ReadAllLinesAsync() | Queremos acesso às linhas por índice |
File.ReadLines() | IEnumerable<string> | Não | File.ReadLinesAsync() (.NET 7+) | Processamos linha por linha, ou o arquivo é grande |
StreamReader.ReadToEnd() | string | Sim | ReadToEndAsync() | Já temos um stream |
StreamReader.ReadLine() | string, ou null no fim | Não | ReadLineAsync() | Precisamos controlar o loop nós mesmos |
StreamReader.ReadBlock() | int, o número de caracteres | Não | ReadBlockAsync() | Queremos blocos de tamanho fixo |
RandomAccess.Read() | int, o número de bytes | Não | RandomAccess.ReadAsync() | Lemos bytes em um deslocamento, possivelmente a partir de várias threads |
Para comparar os métodos, vamos usar a biblioteca BenchmarkDotNet. A classe de benchmark que executa os treze métodos sobre um único arquivo de exemplo de cerca de 98 KB está no código-fonte cujo link aparece no início do artigo.
Agora, vamos executar o benchmark e examinar os resultados no console:
BenchmarkDotNet v0.15.8, Windows 10 (10.0.19045.6466/22H2/2022Update) AMD Ryzen 5 3600 3.60GHz, 1 CPU, 12 logical and 6 physical cores .NET SDK 10.0.302 [Host] : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3 DefaultJob : .NET 10.0.10 (10.0.10, 10.0.1026.32716), X64 RyuJIT x86-64-v3 | Method | Mean | Error | StdDev | Allocated | |------------------------------------- |---------:|---------:|---------:|----------:| | FileReadAllText | 187.3 us | 1.40 us | 1.09 us | 410.83 KB | | StreamReaderReadToEnd | 189.4 us | 3.74 us | 4.60 us | 410.83 KB | | StreamReaderReadBlockWithSpan | 193.8 us | 1.41 us | 1.32 us | 418.71 KB | | StreamReaderReadBlockWithArrayPool | 208.8 us | 0.86 us | 0.76 us | 609.89 KB | | StreamReaderReadBlock | 216.2 us | 3.58 us | 2.99 us | 617.92 KB | | BufferedStreamWithNoFileStreamBuffer | 237.4 us | 3.09 us | 2.89 us | 626.83 KB | | StreamReaderReadLine | 237.8 us | 3.26 us | 2.89 us | 626.78 KB | | BufferedStreamObject | 240.2 us | 4.77 us | 5.30 us | 626.85 KB | | FileReadLines | 248.7 us | 4.82 us | 4.95 us | 626.84 KB | | FileReadAllLines | 248.9 us | 3.79 us | 4.37 us | 650.89 KB | | StreamReaderReadToEndAsync | 539.7 us | 10.65 us | 13.47 us | 415.68 KB | | FileReadAllTextAsync | 551.5 us | 10.91 us | 12.56 us | 420.12 KB | | FileReadLinesAsync | 707.5 us | 13.58 us | 12.70 us | 704.59 KB |
As duas formas mais rápidas de ler um arquivo de texto em C# são File.ReadAllText() e StreamReader.ReadToEnd(). A diferença entre elas aqui é de 2,1 microssegundos, contra margens de erro de 1,4 e 3,7, e uma segunda execução na mesma máquina inverteu a ordem, com 0,5 microssegundo de diferença. Os dois métodos estão empatados, e qualquer um deles é a escolha certa quando queremos o arquivo inteiro como string.
Logo depois, vêm os três métodos com StreamReader.ReadBlock(). O que usa um Span é o mais rápido e usa menos memória que as outras duas variantes.
Os métodos com ReadBlock() são bem rápidos porque leem o conteúdo do arquivo em blocos. Em geral, isso é mais rápido do que ler caractere por caractere ou linha por linha.
Na parte de baixo da classificação síncrona ficam os métodos File.ReadAllLines() e File.ReadLines(), com StreamReader.ReadLine() e as duas variantes de BufferedStream um pouco à frente deles. Os cinco percorrem o arquivo uma linha por vez e o remontam com um StringBuilder, e é esse trabalho por linha que os separa do topo da tabela.
Os três métodos assíncronos ficam em último, com folga, a cerca do triplo do custo dos seus equivalentes bloqueantes. Esse é o resultado esperado em um arquivo pequeno e em cache, em que o custo adicional da E/S assíncrona é a única diferença medida. Isso não é um argumento contra usá-los em uma aplicação web, em que o objetivo é liberar a thread, e não terminar mais cedo.
Vale notar que métodos como o StreamReader.ReadToEnd(), que carrega o arquivo completo na memória, podem ser eficazes para arquivos pequenos. No entanto, com arquivos maiores, esses métodos podem levar a uma alocação de memória considerável, o que pode afetar o desempenho geral da aplicação.
Qual método usar para um arquivo de texto grande?
Para um arquivo grande demais para caber com folga na memória, use File.ReadLines() ou sua forma assíncrona, File.ReadLinesAsync(). Os dois leem de forma incremental, então o uso de memória fica estável por maior que seja o arquivo, e o processo não quebra com um arquivo que não conseguiria carregar.
File.ReadLinesAsync() retorna IAsyncEnumerable<string>.
A regra prática é curta. Se vamos precisar do arquivo inteiro como uma única string de qualquer jeito, lemos tudo de uma vez e aceitamos a alocação. Se percorremos o arquivo registro por registro, descartando cada um pelo caminho, lemos de forma incremental e nunca chamamos um método que retorne o arquivo inteiro de uma vez.
Vale a pena conhecer dois recursos extras para arquivos desse tamanho. FileOptions.SequentialScan avisa o sistema operacional de que estamos lendo do início ao fim, o que ele pode tomar como uma dica de cache. E RandomAccess lê bytes em um deslocamento (offset) explícito a partir de um identificador (handle) de arquivo, que é a ferramenta certa quando várias threads leem partes diferentes de um mesmo arquivo grande.
Um detalhe a observar nos exemplos acima: todos os métodos de leitura incremental desta página remontam o arquivo inteiro em um StringBuilder antes de retorná-lo, que é exatamente o que a leitura incremental deveria evitar. Eles fazem isso para que o benchmark compare os treze métodos com um trabalho idêntico. Um código real de leitura incremental processa cada linha dentro do loop e a descarta, e é daí que vem de fato o uso estável de memória.
Conclusão
Neste artigo, vimos treze métodos que podemos usar para ler um arquivo de texto em C#, dez deles bloqueantes e três assíncronos. No final, comparamos o desempenho de todos os métodos, em tempo e em memória, com testes de benchmark. Pelos nossos resultados, File.ReadAllText() e StreamReader.ReadToEnd() terminam empatados no topo, e os dois são a escolha certa quando queremos o arquivo inteiro como uma única string. Quando o arquivo é grande demais para isso, File.ReadLines() é o método a usar.
A escrita é a outra metade do trabalho, e também há várias formas de sobrescrever um arquivo em C#, agora que sabemos ler um.
Testado com .NET 10 e BenchmarkDotNet 0.15.8.
