Menu

Escribir y leer archivos en C#: File, StreamWriter y StreamReader

Cómo escribir en un archivo y leer un archivo en C#: File.WriteAllText, ReadAllText, AppendAllText, WriteAllLines y ReadAllLines para archivos completos, StreamWriter y StreamReader para los grandes, rutas con Path.Combine, directorios, codificaciones y las excepciones que tiene que manejar el código que trabaja con archivos.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

El namespace System.IO tiene todo lo necesario para trabajar con archivos. Para la mayoría de las tareas basta con una llamada a la clase static File: abre el archivo, lo lee o lo escribe, y lo cierra. Para archivos grandes o un control más fino, usa StreamReader y StreamWriter. Añade using System.IO; a todos los ejemplos de abajo.

Escribir y leer un archivo completo

File.WriteAllText crea un archivo (o sobrescribe uno existente) con el texto que le das. File.ReadAllText devuelve el archivo entero como un solo string. File.AppendAllText añade al final:

Salida:

True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False

Una ruta relativa como "notes.txt" se resuelve respecto al directorio actual del proceso, que no siempre es la carpeta donde vive tu programa (un servicio, una tarea programada o un ejecutor de pruebas pueden arrancar en otro sitio). Para los archivos que se distribuyen con tu aplicación, construye la ruta a partir de AppContext.BaseDirectory.

WriteAllText sobrescribe sin preguntar. Si perder el contenido anterior importa, comprueba antes File.Exists o escribe en un archivo temporal y después muévelo a su sitio con File.Move.

Líneas: WriteAllLines, ReadAllLines y ReadLines

Los archivos de registros, uno por línea, son tan habituales que tienen sus propios métodos. WriteAllLines escribe cada string seguido de un salto de línea; ReadAllLines devuelve un string[]:

Salida:

4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00

La diferencia entre los dos lectores importa según el tamaño. ReadAllLines lee el archivo entero en memoria antes de devolver nada. ReadLines devuelve un IEnumerable<string> perezoso que lee a medida que recorres, así que un foreach sobre un log de 5 GB usa unos pocos kilobytes. Con LINQ, ReadLines(...).Where(...).Take(10) deja de leer en cuanto tiene diez coincidencias.

Parsear CSV con Split(',') funciona con archivos sencillos que has generado tú. El CSV del mundo real tiene campos entre comillas que contienen comas; usa una biblioteca como CsvHelper para esos.

Fíjate en el InvariantCulture de decimal.Parse: sin él, parsear "4.50" en una máquina configurada en alemán o en portugués lee el punto como separador de miles.

StreamWriter y StreamReader

Los métodos de File abren y cierran el archivo en cada llamada. Para escribir muchas piezas a lo largo del tiempo, o para leer un archivo demasiado grande para la memoria sin LINQ, abre un stream una sola vez. Envuélvelo siempre en using para que se cierre aunque se lance una excepción; un writer sin cerrar puede no volcar nunca su buffer, y el archivo acaba vacío o cortado.

Salida:

2: WARN  disk 85% full
4: ERROR connection refused

ReadLine devuelve null al final del archivo, que es lo que termina el bucle while. Un StreamWriter guarda su salida en un buffer y la escribe en el disco cuando el buffer se llena, cuando llamas a Flush() y cuando se libera; el bloque using garantiza lo último.

Desde C# 8, una declaración using libera el stream al final del bloque que la contiene, sin las llaves extra:

using var writer = new StreamWriter(path);   // C# 8: disposed when the method returns
writer.WriteLine("INFO  server started");

Rutas y directorios

Construye las rutas con Path.Combine en lugar de concatenar strings. Inserta el separador correcto para el sistema operativo (\ en Windows, / en Linux y macOS) y no lo duplica. La clase Path también descompone rutas, y Directory crea y lista carpetas:

Salida:

february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False

Una trampa: si un argumento posterior de Path.Combine es una ruta con raíz (empieza por / o \, o por una letra de unidad en Windows), todo lo anterior se descarta, así que Path.Combine("reports", "/2026") es /2026. Pasa partes relativas, o usa Path.Join (.NET Core 3.0 y posteriores), que nunca descarta nada.

Directory.GetFiles devuelve los archivos en el orden en que los da el sistema de archivos, que cambia entre Windows y Linux, así que ordena el resultado cuando el orden importe. Directory.EnumerateFiles es la versión perezosa, la misma relación que hay entre ReadLines y ReadAllLines. Pasa SearchOption.AllDirectories para incluir las subcarpetas.

Manejar los errores

Las operaciones con archivos fallan por motivos ajenos a tu programa: falta el archivo, la carpeta no existe, otro proceso lo tiene abierto, el disco está lleno, los permisos no son los correctos. Cada uno tiene su propio tipo de excepción. FileNotFoundException y DirectoryNotFoundException derivan de IOException, así que captúralas antes que ella; UnauthorizedAccessException no, así que necesita su propio catch:

Salida:

missing file, using defaults
missing folder, using defaults
theme=dark

Llamar a File.Exists antes de leer parece más sencillo, pero no elimina la necesidad del try: el archivo puede desaparecer, o bloquearlo otro programa, en el momento que hay entre la comprobación y la lectura. Usa Exists para decidir qué hacer, y catch para sobrevivir a lo que de verdad ocurra.

Codificación

File.WriteAllText, WriteAllLines y StreamWriter escriben por defecto UTF-8 sin marca de orden de bytes, y los lectores detectan UTF-8, UTF-16 y UTF-32 a partir de una marca de orden de bytes si la hay. Pasa un Encoding cuando un archivo deba estar en un formato concreto:

Salida:

plain.txt   16 bytes, reads back unchanged: True
bom.txt     19 bytes, reads back unchanged: True
utf16.txt   30 bytes, reads back unchanged: True

é y ã ocupan dos bytes cada una en UTF-8, por eso 14 caracteres necesitan 16 bytes, y los tres archivos se leen bien porque el lector reconoce la marca de orden de bytes. Encoding.UTF8 parece el valor por defecto pero añade una marca de orden de bytes, que algunas herramientas (importadores de CSV antiguos, scripts de shell) muestran como basura al principio de la primera línea; usa new UTF8Encoding(false) para un UTF-8 explícito sin ella. Leer un archivo con la codificación equivocada no lanza ninguna excepción: produce caracteres de sustitución, así que averigua qué codificación usa un archivo que no has creado tú.

E/S de archivos async

En las aplicaciones web y en el código de interfaz, bloquear un hilo con E/S de disco es un desperdicio. .NET Core 2.0 y posteriores tienen versiones async de los 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 y StreamWriter.WriteLineAsync funcionan igual sobre los streams.

Errores comunes

  • No liberar un stream. Sin using, el texto del buffer puede no llegar nunca al disco y el archivo se queda bloqueado hasta que se ejecute el recolector de basura.
  • Construir rutas con + "\\" +. Se rompe en Linux y macOS. Usa Path.Combine.
  • Cargar archivos enormes con ReadAllText o ReadAllLines. Léelos por partes con ReadLines o un StreamReader.
  • Depender del directorio actual. Depende de cómo se arrancó el programa. Usa rutas absolutas o AppContext.BaseDirectory.
  • Parsear números de archivos con la cultura de la máquina. Pasa CultureInfo.InvariantCulture en los archivos de datos.
  • Esperar que File.Exists evite las excepciones. El archivo puede cambiar entre la comprobación y el uso.

Preguntas frecuentes

¿Cómo escribo texto en un archivo en C#?

File.WriteAllText("notes.txt", text); crea el archivo, o lo sobrescribe si existe, escribe el string en UTF-8 y lo cierra. Usa File.WriteAllLines(path, lines) para una colección de líneas y File.AppendAllText(path, text) para añadir al final en lugar de sustituir. Los tres están en System.IO.

¿Cómo leo un archivo de texto en C#?

string text = File.ReadAllText("notes.txt"); lee el archivo entero en un solo string, y string[] lines = File.ReadAllLines(path); lo divide en líneas. Para archivos grandes, usa File.ReadLines(path) en un foreach, que lee una línea cada vez en lugar de cargarlo todo, o un StreamReader.

¿Cómo añado texto a un archivo en C#?

File.AppendAllText(path, text) añade texto al final del archivo y lo crea si no existe; File.AppendAllLines(path, lines) hace lo mismo con líneas. Con un stream, ábrelo en modo de añadir: new StreamWriter(path, append: true). Recuerda añadir tú Environment.NewLine o \n al añadir texto.

¿Cómo leo un archivo línea por línea en C#?

foreach (string line in File.ReadLines(path)) { ... } lee el archivo una línea cada vez, así que el uso de memoria se mantiene plano incluso con logs de gigabytes. El equivalente antiguo es un StreamReader en un bloque using con while ((line = reader.ReadLine()) != null). Evita ReadAllLines con archivos grandes: carga antes todas las líneas en un array.

¿Cómo compruebo si existe un archivo en C#?

File.Exists(path) devuelve true si el archivo existe y el programa puede verlo, y Directory.Exists(path) hace lo mismo con las carpetas. No confíes en él para evitar excepciones: el archivo puede borrarse o bloquearse entre la comprobación y la lectura, así que maneja igualmente FileNotFoundException e IOException alrededor de la operación real con el archivo.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR