Vamos a ver trece formas de leer un archivo de texto en .NET, y dos de ellas sirven para casi todos los casos. File.ReadAllText() lee el archivo completo en un único string. File.ReadLines() lo lee como un stream, una línea cada vez, así que la memoria se mantiene constante por grande que sea el archivo.

Todo lo demás es una variación. StreamReader nos ofrece las mismas lecturas a un nivel más bajo cuando ya tenemos un stream; ReadBlock() lee bloques de tamaño fijo; BufferedStream añade un segundo búfer encima del que ya tiene FileStream, y cada lectura síncrona tiene su equivalente asíncrono.

Cuál es la más rápida depende del archivo. Más abajo medimos el rendimiento de las trece con un archivo de unos 98 KB, en el que File.ReadAllText() y StreamReader.ReadToEnd() terminan empatados. Con un archivo que no cabe en memoria, la pregunta cambia, y la respuesta también.

Para descargar el código fuente de este artículo, puedes visitar nuestro repositorio de GitHub.

Preparación del archivo de origen

Antes de ver los distintos métodos de acceso, vamos a crear un archivo de origen que podamos usar en todos los casos.

Primero, vamos a añadir una cadena a una clase de pruebas del proyecto:

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.
        """;

Usaremos esta cadena para crear un archivo temporal en la clase de pruebas:

private static readonly string TempFilePath = Path.GetTempFileName();

El método GetTempFileName() de la clase Path crea un archivo temporal en la carpeta temporal. Después, devuelve la ruta completa del archivo. Si preferimos no pasar a nuestros métodos ninguna ruta absoluta, hay más de una forma de leer un archivo de texto sin especificar la ruta completa.

En el constructor de la clase de pruebas, escribimos la cadena en este archivo con File.WriteAllText(TempFilePath, ExpectedText). Este archivo será el archivo de texto de ejemplo para los métodos de prueba.

Hay una suposición que recorre todo el ejemplo: el archivo no está vacío. Seis de los métodos siguientes terminan restando Environment.NewLine.Length a la longitud de un StringBuilder para quitar el salto de línea final, y con un archivo de cero bytes ese objeto está vacío y la resta lanza una ArgumentOutOfRangeException. Cubrir ese caso en seis sitios ocultaría lo esencial de cada ejemplo, así que los ejemplos se quedan en su tamaño didáctico y, en su lugar, dejamos constancia aquí de esa suposición.

Leer un archivo de texto con el método ReadAllLines

Con todo configurado, veamos el primer método para leer un archivo de texto. Como todos los métodos siguientes, va en una clase WaysToReadATextFileInCsharp cuyo constructor recibe la ruta del archivo y la guarda en un campo _sampleFilePath, con using System.Buffers; y using System.Text; al principio del archivo:

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();
}

Aquí, invocamos el método File.ReadAllLines() para leer en memoria todas las líneas del archivo de texto. Este método lee todas las líneas del archivo y las convierte en elementos de un array de cadenas. Este array de cadenas se nos devuelve una vez leído el archivo completo.

Después, creamos un objeto StringBuilder que contendrá el texto del archivo.

Usamos el método StringBuilder.AppendLine() para añadir las líneas a la cadena final. Siempre que usamos este método, la cadena resultante puede diferir del contenido original del archivo.

Estas diferencias pueden surgir porque, de forma predeterminada, el método AppendLine() usa el salto de línea que establece Environment.NewLine. Si, por ejemplo, el archivo usa \r como final de línea y el sistema actual usa \r\n, la cadena devuelta podría no reflejar fielmente el texto inicial.

Después, quitamos el último salto de línea restando el valor de la propiedad Environment.NewLine.Length a la longitud del stringBuilder.

Por último, construimos y devolvemos la cadena llamando al método ToString().

Cómo leer un archivo de texto con el método ReadAllText

A continuación, el método ReadAllText():

public string UseFileReadAllText() => File.ReadAllText(_sampleFilePath);

En este método, invocamos el método File.ReadAllText() con la ruta del archivo de ejemplo como argumento. Con este método conciso, podemos leer directamente todas las líneas del archivo y devolverlas como una sola cadena.

¿Cómo leer un archivo de texto línea a línea en C#?

File.ReadLines() lee un archivo de texto línea a línea sin cargar el archivo completo en memoria. Devuelve un IEnumerable<string>, y cada paso del bucle extrae la siguiente línea del stream.

Esa lectura diferida es toda la diferencia entre este método y File.ReadAllLines(). ReadAllLines() lee primero el archivo completo y devuelve un string[], así que un log de dos gigabytes acaba entero en memoria de una vez. Y ocupa todavía más: las cadenas de .NET son UTF-16, por lo que un archivo ASCII ocupa aproximadamente el doble al cargarse. ReadLines() nunca guarda más que la línea actual.

StreamReader.ReadLine() hace lo mismo un nivel más abajo. Abrimos nosotros el lector y llamamos a ReadLine() hasta que devuelve null, que es lo que queremos cuando el bucle tiene que detenerse antes de tiempo, saltar hacia delante o llevar una cuenta sobre la marcha.

Con un archivo pequeño, los tres son intercambiables y la elección es cuestión de gustos. Con un archivo que no cabe en memoria, solo los dos diferidos son una opción viable.

Para empezar, vamos a definir un 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();
}

Aquí, usamos el método File.ReadLines() para recorrer el archivo de texto línea a línea.

Este método lee las líneas del archivo y devuelve una instancia de IEnumerable<string> que podemos usar para iterar sobre el archivo. A diferencia del método File.ReadAllLines(), este método no carga el archivo completo en memoria. En su lugar, lo lee línea a línea, de forma diferida.

Por tanto, al iterar sobre el IEnumerable, cada paso nos entrega la siguiente línea. El lector sigue llenando un búfer interno, así que no se trata de una lectura de disco por línea; lo que se mantiene constante es cuánto del archivo tenemos en memoria, no cuántas veces accedemos al disco. Después del bucle, quitamos el último salto de línea del stringBuilder.

Por último, construimos y devolvemos la cadena llamando al método ToString().

Leer línea a línea es también la forma en que hacemos la mayor parte del trabajo con líneas, desde filtrar registros hasta contar las líneas de un archivo de texto sin tener nada de él en memoria.

Leer un archivo de texto con el método StreamReader.ReadLine

Ahora, vamos a hablar del 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();
}

Aquí, leemos las líneas del archivo una a una con un objeto StreamReader.

Empezamos inicializando una instancia de StreamReader a la que pasamos la ruta del archivo de ejemplo. Usamos la instrucción using para garantizar que el StreamReader se libere correctamente al terminar la ejecución del método.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

A continuación, creamos un objeto StringBuilder que usaremos en el bucle.

La acción principal ocurre dentro del bucle while. En la condición del bucle, usamos la coincidencia de patrones (pattern matching) para leer las líneas del archivo. A medida que leemos cada línea, la asignamos a la variable fileLine. Después, añadimos esta línea a la instancia de stringBuilder con el método stringBuilder.AppendLine(). Esto continúa hasta que llegamos al final del archivo y StreamReader devuelve un valor null.

De nuevo, cuando terminamos de leer el archivo, nos aseguramos de quitar el último salto de línea al final del stringBuilder. Después, construimos y devolvemos la cadena llamando al método ToString().

Leer un archivo de texto con el método ReadToEnd

Seguimos con el método ReadToEnd():

public string UseStreamReaderReadToEnd()
{
    using var streamReader = new StreamReader(_sampleFilePath);

    return streamReader.ReadToEnd();
}

Aquí, creamos una instancia de StreamReader con el archivo de ejemplo.

Después, leemos todo el contenido del stream invocando el método StreamReader.ReadToEnd().

Con este método, tomamos otro camino: leemos todo el contenido del archivo directamente en memoria, de una sola vez.

Usar el método ReadBlock

Además, podemos usar el método StreamReader.ReadBlock() para leer un archivo 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();
}

Primero, creamos un StreamReader para el archivo que queremos leer. Después, creamos un array de 4096 caracteres, ya que es el tamaño de búfer predeterminado que usa FileStream.

También declaramos un entero, numberRead, para guardar el número de caracteres que leemos en cada iteración del bucle. A continuación, creamos un objeto StringBuilder que usaremos en el bucle.

En cada iteración del bucle, comprobamos si el número de caracteres leídos sigue siendo positivo. Si lo es, significa que hay más contenido en el stream. Así que leemos los datos y los añadimos a la instancia de StringBuilder.

Dentro del bucle, pasamos al método Append() los caracteres desde el principio del array de búfer hasta el carácter de índice numberRead - 1. Así, añadimos los caracteres que acabamos de leer con el método ReadBlock(). Repetimos este proceso hasta que el valor de numberRead baja a cero o menos, lo que indica el final del archivo, y entonces salimos del bucle.

Al terminar el bucle while, construimos y devolvemos la cadena llamando al método ToString().

Además, podemos usar la clase ArrayPool del espacio de nombres System.Buffers para obtener el búfer:

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();
}

Aquí, el búfer procede de una instancia compartida de la clase ArrayPool<char>. Obtenemos esta instancia con la propiedad Shared de la clase ArrayPool.

ArrayPool es una clase de .NET que nos ayuda a crear y reutilizar instancias de arrays de forma eficiente. Con esta clase, reducimos las necesidades de memoria de la aplicación y mejoramos su rendimiento general. Explicamos con más detalle cómo reduce ArrayPool las asignaciones de memoria en un artículo aparte.

Después, llamamos al método Rent() para obtener un búfer de longitud 4096. Cuando terminamos con el búfer, lo devolvemos al ArrayPool invocando el método Return(). Al final, construimos y devolvemos la cadena llamando al método ToString().

Leer un archivo de texto con el método ReadBlock y la estructura Span

Veamos ahora otra forma de usar el método ReadBlock(), incorporando la estructura 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();
}

En este método, convertimos el búfer en un Span<char> con el método AsSpan().

Después, en la definición del bucle while, simplificamos la invocación del método ReadBlock() pasándole solo el búfer, ahora como Span. A continuación, recorremos el archivo y añadimos sus caracteres al objeto stringBuilder.

Cuando termina el bucle, construimos y devolvemos la cadena.

Cómo leer un archivo de texto con un objeto BufferedStream

A continuación, veamos cómo leer un archivo de texto creando un 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();
}

En este método, primero creamos un nuevo StringBuilder para reunir el texto. Después, creamos una instancia abierta de FileStream a partir del archivo de ejemplo. Con ese FileStream, configuramos un BufferedStream. Y, con ese BufferedStream, creamos un StreamReader.

Ahora viene la acción de verdad: el bucle while. Mientras sigamos obteniendo líneas con el método StreamReader.ReadLine(), las añadimos al stringBuilder con el método AppendLine(). En cuanto fileLine pasa a ser null, es señal de que hemos terminado de leer del bufferedStream. También significa que estamos al final del archivo, y salimos del bucle.

Cuando terminamos con todo, quitamos el último salto de línea y construimos y devolvemos la cadena llamando al método ToString().

Aunque este método lee correctamente, la clase FileStream usa de forma predeterminada un búfer interno de 4096 bytes. Esto puede provocar un doble búfer: el del FileStream y el del BufferedStream.

Leer un archivo de texto con un objeto BufferedStream y el búfer de FileStream desactivado

Por último, veamos cómo podemos resolver la situación de “doble búfer”. Para ello, vamos a desactivar el búfer de 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();
}

Establecemos BufferSize en 0 para desactivar el búfer, y lo hacemos mediante la sobrecarga FileStreamOptions en lugar del constructor antiguo que recibe las opciones como argumentos separados. Es una elección deliberada. La referencia de Microsoft Learn de FileStreamOptions.BufferSize dice que “0 o 1 significa que el almacenamiento en búfer debe desactivarse”, mientras que la sobrecarga del constructor FileStream que recibe un argumento bufferSize lo documenta como un valor mayor que cero e indica una ArgumentOutOfRangeException para el cero. Las dos formas se ejecutan, pero solo una está documentada para hacer lo que enseña este apartado. Después, seguimos el mismo proceso y devolvemos la cadena.

¿Cómo leer un archivo de texto de forma asíncrona en C#?

Cada lectura síncrona de esta página tiene su equivalente asíncrono, y dentro de una aplicación web la asíncrona es la opción por defecto correcta. Una lectura bloqueante deja aparcado un hilo (thread) del thread pool durante toda la operación de disco, y ese hilo podría haber estado atendiendo una solicitud.

El inconveniente es que solo obtenemos eso si el archivo se abrió preparado para ello. Los métodos File.*Async sí lo abren así; un StreamReader construido a partir de una ruta no, y entonces el runtime simula la asincronía en el thread pool.

File.ReadAllTextAsync() devuelve un Task<string> y es el sustituto directo de File.ReadAllText(). File.ReadAllLinesAsync() devuelve un Task<string[]>.

The Web API Production Checklist, ebook gratuito en inglés

Ebook gratis

¿Tu Web API está lista para producción?

33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.

El ebook está en inglés.

Descarga la checklist gratis

PDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.

File.ReadLinesAsync() es el diferido. Llegó con .NET 7 y devuelve un IAsyncEnumerable<string>, así que lo consumimos con await foreach y seguimos sin tener nunca más de una línea en memoria.

Asíncrono no es lo mismo que más rápido. Con nuestro archivo de benchmark, las lecturas asíncronas tardan aproximadamente el triple que sus equivalentes bloqueantes. Lo que aportan es un hilo libre, no una espera más corta.

Vamos a empezar con el sustituto directo de File.ReadAllText():

public async Task<string> UseFileReadAllTextAsync()
    => await File.ReadAllTextAsync(_sampleFilePath);

Aquí no hay nada que preparar: el método abre el archivo con la E/S asíncrona activada, lo lee hasta el final y devuelve la cadena.

Después, el diferido:

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 reproduce UseFileReadLines() línea a línea, con await foreach en lugar de foreach, así que el benchmark compara cosas equivalentes. En código real haríamos el trabajo dentro del bucle y nunca llegaríamos a construir la cadena. El patrón es el mismo que usamos al consumir un IAsyncEnumerable con yield.

Por último, la versión con StreamReader, que es la que requiere 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();
}

Construimos nosotros el FileStream para poder establecer FileOptions.Asynchronous. Si escribimos new StreamReader(path) y después esperamos ReadToEndAsync() con await, el código compila y devuelve el texto correcto, pero el archivo se abrió sin ese indicador, así que la lectura solo es asíncrona desde el punto de vista de nuestro código: internamente, un hilo del thread pool hace la espera bloqueante que intentábamos evitar. ReadLineAsync() y ReadBlockAsync() reproducen del mismo modo sus equivalentes bloqueantes, y tienen la misma salvedad.

¿Cuál es la forma más rápida de leer un archivo de texto en C#?

Con nuestro archivo de benchmark, dos métodos comparten el primer puesto de la tabla: File.ReadAllText() y StreamReader.ReadToEnd(). Los dos hacen una sola pasada sobre el archivo y devuelven una cadena, asignan la misma cantidad de memoria y terminan a un par de microsegundos el uno del otro, lo que está dentro del ruido entre ejecuciones.

Lo que pedimos importa tanto como el método al que llamamos. Ocho de los diez métodos síncronos leen líneas o bloques y después reconstruyen una sola cadena mediante un StringBuilder, así que pagan por la reconstrucción además de por la lectura. ReadToEnd() y File.ReadAllText() hacen ese paso dentro del runtime, directamente desde el búfer del propio StreamReader, y eso explica en parte su ventaja.

El tamaño importa más que ambas cosas. El archivo medido aquí ocupa unos 98 KB, lo bastante poco para quedarse en la caché del sistema operativo y para que cargarlo entero no cueste casi nada. Si ocupara un gigabyte, el método más rápido pasaría a ser el que nunca lo carga entero.

MétodoDevuelveGuarda el archivo completo en memoriaForma asíncronaÚsalo cuando
File.ReadAllText()stringSíFile.ReadAllTextAsync()Queremos el archivo completo como una sola cadena
File.ReadAllLines()string[]SíFile.ReadAllLinesAsync()Queremos acceder a las líneas por índice
File.ReadLines()IEnumerable<string>NoFile.ReadLinesAsync() (.NET 7+)Procesamos línea a línea, o el archivo es grande
StreamReader.ReadToEnd()stringSíReadToEndAsync()Ya tenemos un stream
StreamReader.ReadLine()string, o null al finalNoReadLineAsync()Necesitamos controlar el bucle nosotros mismos
StreamReader.ReadBlock()int, el número de caracteresNoReadBlockAsync()Queremos bloques de tamaño fijo
RandomAccess.Read()int, el número de bytesNoRandomAccess.ReadAsync()Leemos bytes en un desplazamiento, quizá desde varios hilos

Para comparar los métodos, vamos a usar la biblioteca BenchmarkDotNet. La clase de benchmark que ejecuta los trece métodos sobre un archivo de ejemplo de unos 98 KB está en el código fuente enlazado al principio del artículo.

Ahora, vamos a ejecutar el benchmark y a examinar los resultados en la consola:

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 |

Las dos formas más rápidas de leer un archivo de texto en C# son File.ReadAllText() y StreamReader.ReadToEnd(). Aquí, la diferencia entre ellas es de 2.1 microsegundos, frente a márgenes de error de 1.4 y 3.7, y una segunda ejecución en la misma máquina invirtió el orden y las dejó a 0.5 microsegundos de distancia. Están empatadas, y cualquiera de las dos es la elección correcta cuando queremos el archivo completo como una cadena.

A continuación, tenemos los tres métodos StreamReader.ReadBlock(). El que usa un Span es el más rápido y usa menos memoria que las otras dos variantes.

Los métodos ReadBlock() son bastante rápidos porque leen el contenido del archivo por bloques. Por lo general, esto es más rápido que leerlo carácter a carácter o línea a línea.

Al final de la clasificación síncrona están los métodos File.ReadAllLines() y File.ReadLines(), con StreamReader.ReadLine() y las dos variantes de BufferedStream un poco por delante. Los cinco recorren el archivo línea a línea y lo reconstruyen mediante un StringBuilder, y ese trabajo por línea es lo que los separa de la parte alta de la tabla.

Los tres métodos asíncronos quedan los últimos con mucha diferencia y cuestan aproximadamente el triple que sus equivalentes bloqueantes. Es lo esperable con un archivo pequeño y en caché, donde el costo adicional de la E/S asíncrona es la única diferencia que se mide. No es un argumento en contra de usarlos en una aplicación web, donde lo importante es liberar el hilo y no terminar antes.

Conviene tener en cuenta que métodos como StreamReader.ReadToEnd(), que carga el archivo completo en memoria, pueden ser eficaces con archivos pequeños. Sin embargo, con archivos más grandes, estos métodos pueden provocar una asignación de memoria considerable, lo que puede afectar al rendimiento general de la aplicación.

¿Qué método conviene usar con un archivo de texto grande?

Si el archivo es demasiado grande para tenerlo en memoria con holgura, usa File.ReadLines() o su forma asíncrona, File.ReadLinesAsync(). Los dos leen como un stream, así que la memoria se mantiene constante por mucho que crezca el archivo, y el proceso no se cae con un archivo que no le cabe.

Dos formas de leer un archivo de texto en C#: leerlo entero en una cadena y leerlo como un stream, una línea cada vez.

File.ReadLinesAsync() devuelve IAsyncEnumerable<string>.

La regla práctica es breve. Si de todos modos necesitamos el archivo completo como una sola cadena, lo leemos entero y aceptamos la asignación de memoria. Si recorremos el archivo registro a registro y vamos descartando sobre la marcha, lo leemos como un stream y nunca llamamos a un método que devuelva el archivo completo de una vez.

Con este tamaño, vale la pena conocer dos extras. FileOptions.SequentialScan indica al sistema operativo que leemos de principio a fin, algo que puede tomar como una pista para la caché. Y RandomAccess lee bytes en un desplazamiento explícito a partir de un identificador (handle) de archivo, que es la herramienta adecuada cuando varios hilos leen partes distintas de un mismo archivo grande.

Hay algo en lo que fijarse en los ejemplos anteriores: todos los métodos de streaming de esta página vuelven a montar el archivo completo en un StringBuilder antes de devolverlo, que es justo lo que el streaming debería evitar. Lo hacen para que el benchmark compare los trece métodos con un trabajo idéntico. El código de streaming real procesa cada línea dentro del bucle y la descarta, y de ahí sale en realidad el perfil de memoria constante.

Conclusión

En este artículo, hemos visto trece métodos que podemos usar para leer un archivo de texto en C#, diez de ellos bloqueantes y tres asíncronos. Al final, hemos comparado el rendimiento en tiempo y en memoria de todos los métodos mediante benchmarks. Según nuestros resultados, File.ReadAllText() y StreamReader.ReadToEnd() terminan empatados en primer lugar, y los dos son la opción correcta cuando queremos el archivo completo como una sola cadena. Cuando el archivo es demasiado grande para eso, el método indicado es File.ReadLines().

Escribir es la otra mitad del trabajo, y también hay varias formas de sobrescribir un archivo en C# una vez que sabemos leerlo.

Probado con .NET 10 y BenchmarkDotNet 0.15.8.