Hasta ahora, todo el estado de BiblioTech —el catálogo, los socios, el historial de
préstamos— vive únicamente en la memoria del programa: en cuanto el proceso termina, todo
desaparece con él. Esta lección abre el Módulo 5 (Trabajando con Datos) atacando exactamente
ese problema por el camino más directo: leer y escribir ficheros en disco con el namespace
System.IO. Aprenderás a comprobar si un fichero existe, a leerlo y escribirlo de una vez o
línea a línea, a liberar correctamente los recursos que abres, y a manejar los errores propios
de la E/S (entrada/salida). Al final de la lección, Biblioteca sabrá guardar y recuperar su
catálogo completo en un fichero de texto plano, la primera forma —muy básica— de persistencia
real de todo el curso.
Contenido
- El namespace
System.IOy los dos niveles de trabajo con ficheros - Comprobar la existencia de un fichero:
File.Exists - Todo de una vez:
File.ReadAllText/File.WriteAllTextyFile.ReadAllLines/File.WriteAllLines - Ficheros grandes:
StreamReaderyStreamWriter usingeIDisposable: liberar recursos correctamente- Manejo de excepciones de E/S:
IOExceptionyFileNotFoundException - Persistiendo el catálogo de BiblioTech en texto plano
- El namespace
System.IO y los dos niveles de trabajo con ficheros
System.IO y los dos niveles de trabajo con ficherosTodo lo relacionado con ficheros y directorios en .NET vive en el namespace System.IO. Dentro
de él conviven dos niveles de trabajo, pensados para necesidades distintas:
| Nivel | Clases principales | Cuándo usarlo |
|---|---|---|
| Alto nivel, "todo de una vez" | File (métodos estáticos) |
Ficheros pequeños o medianos que caben cómodamente en memoria |
| Bajo nivel, "poco a poco" | StreamReader, StreamWriter |
Ficheros grandes, o cuando se quiere procesar línea a línea sin cargarlo entero |
Ambos niveles acaban usando el mismo mecanismo por debajo (un Stream que representa el flujo
de bytes hacia o desde el disco), pero File te ahorra la gestión manual del flujo cuando el
tamaño del fichero no es un problema. Esta lección presenta primero File, por ser el punto de
entrada más sencillo, y después StreamReader/StreamWriter para cuando ese enfoque no basta.
- Comprobar la existencia de un fichero:
File.Exists
File.ExistsAntes de leer un fichero, conviene comprobar que existe; antes de escribirlo, no siempre hace
falta (File.WriteAllText lo crea si no existe), pero sí es una buena práctica antes de
cualquier lectura:
string ruta = "catalogo.txt";
if (File.Exists(ruta))
{
Console.WriteLine("El fichero de catalogo ya existe.");
}
else
{
Console.WriteLine("Todavia no se ha guardado ningun catalogo.");
}File.Exists(ruta) devuelve true/false sin lanzar ninguna excepción, incluso si la ruta
tiene un formato inválido o el directorio no existe (en esos casos simplemente devuelve
false). Es la forma segura de anticiparse a un fichero ausente, en vez de intentar leerlo
directamente y capturar la excepción que eso provocaría.
- Todo de una vez:
File.ReadAllText/File.WriteAllText y File.ReadAllLines/File.WriteAllLines
File.ReadAllText/File.WriteAllText y File.ReadAllLines/File.WriteAllLinesLa clase estática File ofrece cuatro métodos de uso inmediato para trabajar con el contenido
completo de un fichero:
| Método | Lee/escribe | Devuelve/recibe |
|---|---|---|
File.WriteAllText(ruta, contenido) |
Escribe | Un único string (crea o sobrescribe el fichero entero) |
File.ReadAllText(ruta) |
Lee | Un único string con todo el contenido |
File.WriteAllLines(ruta, lineas) |
Escribe | Un IEnumerable<string>, una línea por elemento |
File.ReadAllLines(ruta) |
Lee | Un string[], una posición por línea del fichero |
// Escribir todo el contenido de golpe
File.WriteAllText("saludo.txt", "Bienvenido a BiblioTech");
// Leerlo de vuelta, tambien de golpe
string contenido = File.ReadAllText("saludo.txt");
Console.WriteLine(contenido); // "Bienvenido a BiblioTech"
// Escribir varias lineas a partir de un array o una lista
string[] titulos = { "Rayuela", "Ficciones", "El Aleph" };
File.WriteAllLines("titulos.txt", titulos);
// Leerlas de vuelta, una por posicion del array
string[] titulosLeidos = File.ReadAllLines("titulos.txt");
foreach (string titulo in titulosLeidos)
{
Console.WriteLine(titulo);
}Ambos métodos de escritura sobrescriben el fichero si ya existía (no añaden contenido al
final); si necesitas añadir sin borrar lo anterior, existen las variantes File.AppendAllText
y File.AppendAllLines, con la misma forma de uso. Estos cuatro métodos son la opción por
defecto siempre que el fichero sea de un tamaño razonable: internamente abren el fichero,
leen o escriben todo su contenido, y lo cierran automáticamente por ti, sin que tengas que
gestionar nada más.
- Ficheros grandes:
StreamReader y StreamWriter
StreamReader y StreamWriterCuando un fichero es demasiado grande para cargarlo entero en memoria de golpe (piensa en un
.csv con millones de filas), o cuando quieres empezar a procesar sus líneas antes de que
termine de leerse por completo, File.ReadAllText/File.ReadAllLines dejan de ser adecuados:
cargan todo el fichero en memoria antes de devolver el control. StreamReader y
StreamWriter resuelven esto trabajando línea a línea, sin necesidad de tener el fichero
completo en memoria en ningún momento:
// Escribir linea a linea con StreamWriter
StreamWriter escritor = new StreamWriter("catalogo_grande.txt");
escritor.WriteLine("Rayuela;Julio Cortazar");
escritor.WriteLine("Ficciones;Jorge Luis Borges");
escritor.Close(); // libera el fichero; sin esto, los datos podrian no llegar a escribirse
// Leer linea a linea con StreamReader
StreamReader lector = new StreamReader("catalogo_grande.txt");
string? linea;
while ((linea = lector.ReadLine()) != null)
{
Console.WriteLine($"Linea leida: {linea}");
}
lector.Close();ReadLine() devuelve null cuando llega al final del fichero, lo que permite usarlo
directamente como condición de un while: el bucle se repite mientras haya líneas, y termina
solo cuando ReadLine() devuelve null. Este patrón (while ((linea = lector.ReadLine()) != null)) es tan habitual en C# que conviene memorizarlo tal cual.
El ejemplo anterior tiene, a propósito, un problema: si ocurriera una excepción entre la
apertura y el Close(), el fichero quedaría abierto para siempre (un fugado de recursos,
resource leak). El siguiente apartado resuelve exactamente eso.
using e IDisposable: liberar recursos correctamente
using e IDisposable: liberar recursos correctamenteStreamReader y StreamWriter (igual que SqliteConnection, que verás en la lección de
Conectividad con Bases de Datos) representan un recurso externo al programa —un fichero abierto
por el sistema operativo— que debe liberarse explícitamente cuando se termina de usar. Todos
implementan la interfaz IDisposable (recuerda IDisposable de la lección de Constructores y
Destructores, Módulo 3), cuyo método Dispose() libera ese recurso.
Llamar a Dispose() (o a su equivalente Close()) manualmente, como en el apartado anterior,
funciona, pero obliga a acordarse de hacerlo en todos los caminos de salida del método,
incluidos los que pasan por una excepción. La sentencia using automatiza esto por completo:
using (StreamWriter escritor = new StreamWriter("catalogo_grande.txt"))
{
escritor.WriteLine("Rayuela;Julio Cortazar");
escritor.WriteLine("Ficciones;Jorge Luis Borges");
} // aqui se llama automaticamente a escritor.Dispose(), incluso si algo lanza una excepcion dentro del bloque
using (StreamReader lector = new StreamReader("catalogo_grande.txt"))
{
string? linea;
while ((linea = lector.ReadLine()) != null)
{
Console.WriteLine($"Linea leida: {linea}");
}
} // idem: lector.Dispose() se llama siempre al salir del bloqueDesde C# 8 existe además la using declaration, sin llaves propias, que libera el recurso al
final del bloque que la contiene (el método, o el bloque { } en el que se declaró):
using StreamReader lector = new StreamReader("catalogo_grande.txt");
string? linea;
while ((linea = lector.ReadLine()) != null)
{
Console.WriteLine(linea);
}
// lector.Dispose() se llama automaticamente aqui, al final del metodo (o del bloque contenedor)| Forma | Sintaxis | Cuándo liberar el recurso |
|---|---|---|
using (bloque) |
using (var x = ...) { ... } |
Al final de las llaves { } |
using (declaración) |
using var x = ...; |
Al final del método o bloque contenedor |
Manual (Close()/Dispose()) |
Llamada explícita | Cuando el programador se acuerde de llamarlo —frágil, evítalo |
Regla práctica para el resto del curso: cualquier objeto que implemente IDisposable
(ficheros, conexiones a bases de datos, HttpClient...) se declara con using, sin
excepciones, salvo casos muy concretos que quedan fuera del alcance de este curso.
- Manejo de excepciones de E/S:
IOException y FileNotFoundException
IOException y FileNotFoundExceptionCualquier operación de E/S puede fallar por motivos ajenos al programa: el fichero no existe,
el disco está lleno, otro proceso lo tiene bloqueado, no hay permisos suficientes... Todas estas
situaciones se manifiestan mediante excepciones, que se manejan con try/catch, exactamente
como en la lección de Manejo de Excepciones (Módulo 2):
| Excepción | Cuándo se lanza |
|---|---|
FileNotFoundException |
Se intenta abrir para lectura un fichero que no existe |
DirectoryNotFoundException |
El directorio de la ruta indicada no existe |
UnauthorizedAccessException |
No hay permisos suficientes para leer o escribir esa ruta |
IOException |
Clase base de errores de E/S más generales (disco lleno, fichero bloqueado por otro proceso...) |
FileNotFoundException, DirectoryNotFoundException y UnauthorizedAccessException heredan,
de hecho, de IOException, así que un único catch (IOException ex) captura cualquiera de
ellas si no necesitas distinguirlas; si sí lo necesitas, coloca los catch más específicos
primero, como aprendiste en la lección de excepciones:
try
{
string contenido = File.ReadAllText("catalogo.txt");
Console.WriteLine(contenido);
}
catch (FileNotFoundException)
{
Console.WriteLine("Todavia no existe ningun catalogo guardado.");
}
catch (IOException ex)
{
Console.WriteLine($"Error de E/S al leer el catalogo: {ex.Message}");
}En la práctica, comprobar File.Exists de antemano evita la mayoría de los
FileNotFoundException; el resto de excepciones de IOException sí conviene capturarlas
siempre que el programa dependa de que la operación tenga éxito.
- Persistiendo el catálogo de BiblioTech en texto plano
Con todo lo anterior, Biblioteca gana dos métodos nuevos para guardar y recuperar su
catálogo (Catalogo, de la lección de Colecciones) en un fichero de texto plano: una línea por
material, con los campos separados por ;. Como el catálogo mezcla Libro y Revista
(recuerda la lección de Herencia), cada línea empieza con un campo adicional que identifica el
tipo:
class Biblioteca
{
// ... Catalogo, Socios, Prestamos, _sociosPorId, PrestamoRegistrado sin cambios ...
// ... AgregarMaterial, AgregarSocio, BuscarSocioPorId, RegistrarPrestamo, PrestarLibroAsync sin cambios ...
public void GuardarCatalogoTexto(string ruta)
{
using StreamWriter escritor = new StreamWriter(ruta);
foreach (MaterialBibliotecario material in Catalogo)
{
if (material is Libro libro)
{
escritor.WriteLine($"LIBRO;{libro.Titulo};{libro.Autor};{libro.Isbn};{libro.Disponible}");
}
else if (material is Revista revista)
{
escritor.WriteLine($"REVISTA;{revista.Titulo};{revista.Autor};{revista.NumeroEdicion};{revista.Disponible}");
}
}
}
public void CargarCatalogoTexto(string ruta)
{
if (!File.Exists(ruta))
{
throw new FileNotFoundException($"No se encontro el fichero de catalogo: {ruta}");
}
Catalogo.Clear();
using StreamReader lector = new StreamReader(ruta);
string? linea;
while ((linea = lector.ReadLine()) != null)
{
string[] campos = linea.Split(';');
string tipo = campos[0];
string titulo = campos[1];
string autor = campos[2];
bool disponible = bool.Parse(campos[4]);
MaterialBibliotecario material = tipo switch
{
"LIBRO" => new Libro(titulo, autor, campos[3]),
"REVISTA" => new Revista(titulo, autor, int.Parse(campos[3])),
_ => throw new InvalidOperationException($"Tipo de material desconocido: {tipo}")
};
if (!disponible)
{
material.Prestar(); // deja el material marcado como no disponible, igual que estaba al guardarlo
}
AgregarMaterial(material);
}
}
}Algunos detalles de este parseo manual merecen explicarse:
linea.Split(';')(recordando Arrays y Cadenas de Texto, Módulo 1) trocea cada línea en unstring[]usando;como separador; el orden de los campos debe coincidir exactamente entreGuardarCatalogoTexto(al escribir) yCargarCatalogoTexto(al leer).- El
switchde expresión (Pattern Matching, Módulo 4) reconstruye el tipo concreto (LibrooRevista) a partir del primer campo, reutilizando sus constructores tal cual se definieron en la lección de Herencia. CargarCatalogoTextoreutilizaAgregarMaterial, ya existente enBiblioteca, en vez de llamar directamente aCatalogo.Add(...): la lógica de alta queda en un único sitio.- Este formato es deliberadamente frágil: si un título contuviera un
;, elSplitproduciría más campos de los esperados y el parseo fallaría. Es exactamente la limitación que motiva la siguiente lección, Serialización, con un formato (JSON) que no tiene este problema.
flowchart LR
A[Biblioteca.Catalogo en memoria] -->|GuardarCatalogoTexto| B[catalogo.txt]
B -->|CargarCatalogoTexto| C[Biblioteca.Catalogo reconstruido]
Errores Comunes y Consejos
- Olvidar
usingen unStreamReader/StreamWriter: sin él, el fichero puede quedar bloqueado para otros procesos, o los datos escritos pueden no llegar a guardarse en disco si el proceso termina antes de llamar aClose()/Dispose()explícitamente. Usa siempreusing, sin excepción, para cualquier tipo que implementeIDisposable. - Leer un fichero sin comprobar antes
File.Exists: intentarFile.ReadAllTextsobre un fichero inexistente lanzaFileNotFoundException; compruébalo antes, o captura la excepción si prefieres el enfoque "intenta y captura" en vez de "comprueba antes". - Confundir
File.WriteAllText/WriteAllLinescon "añadir al final": ambos sobrescriben el fichero completo si ya existía; usaFile.AppendAllText/File.AppendAllLinessi la intención es añadir contenido sin perder el anterior. - Un separador que puede aparecer dentro de los propios datos: el
;de este formato de texto plano no está pensado para títulos que ya contengan un;; en un caso real habría que escapar el separador o elegir un formato más robusto (el problema que resuelve la lección siguiente). - Consejo: para ficheros pequeños o medianos (la inmensa mayoría de los casos en una
aplicación típica),
File.ReadAllText/WriteAllText/ReadAllLines/WriteAllLinesson más simples y menos propensos a errores que gestionar unStreamReader/StreamWritera mano; resérvalos para ficheros realmente grandes o para procesar línea a línea sin cargarlo entero.
Ejercicios
-
Escribe un fragmento de código que use
File.WriteAllLinespara guardar tres títulos de libro en un ficherotitulos.txt, y despuésFile.ReadAllLinespara leerlos de vuelta y mostrarlos por consola, uno por línea. -
Escribe un método
void MostrarFicheroSiExiste(string ruta)que compruebe conFile.Existssi el fichero existe: si existe, léelo conFile.ReadAllTexty muéstralo por consola dentro de untry/catchque captureIOException; si no existe, muestra un mensaje indicándolo, sin intentar leerlo. -
Construye
GuardarCatalogoTextoyCargarCatalogoTextoenBibliotecatal como se han presentado en esta lección. Crea unaBiblioteca, añade dos libros y una revista, guarda el catálogo encatalogo.txt, crea una segunda instancia deBibliotecavacía, carga el fichero en ella conCargarCatalogoTexto, y comprueba queCatalogo.Countcoincide entre ambas instancias.
Soluciones
string[] titulos = { "Rayuela", "Ficciones", "El Aleph" };
File.WriteAllLines("titulos.txt", titulos);
string[] titulosLeidos = File.ReadAllLines("titulos.txt");
foreach (string titulo in titulosLeidos)
{
Console.WriteLine(titulo);
}
void MostrarFicheroSiExiste(string ruta)
{
if (!File.Exists(ruta))
{
Console.WriteLine($"El fichero '{ruta}' no existe.");
return;
}
try
{
string contenido = File.ReadAllText(ruta);
Console.WriteLine(contenido);
}
catch (IOException ex)
{
Console.WriteLine($"Error de E/S al leer el fichero: {ex.Message}");
}
}
Biblioteca origen = new Biblioteca();
origen.AgregarMaterial(new Libro("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
origen.AgregarMaterial(new Libro("Ficciones", "Jorge Luis Borges", "978-84-376-0496-1"));
origen.AgregarMaterial(new Revista("National Geographic", "Varios autores", 302));
origen.GuardarCatalogoTexto("catalogo.txt");
Biblioteca destino = new Biblioteca();
destino.CargarCatalogoTexto("catalogo.txt");
Console.WriteLine(origen.Catalogo.Count); // 3
Console.WriteLine(destino.Catalogo.Count); // 3
Conclusión
En esta lección has aprendido a trabajar con ficheros usando System.IO: comprobar su
existencia con File.Exists, leer y escribir de golpe con File.ReadAllText/WriteAllText y
ReadAllLines/WriteAllLines, procesar ficheros grandes línea a línea con
StreamReader/StreamWriter, liberar recursos correctamente con using, y manejar las
excepciones propias de la E/S. Biblioteca ya puede guardar y recuperar su catálogo completo
entre ejecuciones, con GuardarCatalogoTexto y CargarCatalogoTexto.
El formato de texto plano con ; como separador, sin embargo, es frágil: no soporta bien
títulos con caracteres especiales, obliga a un parseo manual propenso a errores, y no escala
bien a estructuras de datos más complejas (como préstamos, que enlazan un libro con un socio).
La siguiente lección, Serialización, sustituye este parseo manual por System.Text.Json, un
formato estándar que resuelve estos problemas de raíz y que reutilizará, precisamente, los
ficheros que ya sabes leer y escribir gracias a esta lección.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
