Fins ara, tot l'estat de BiblioTech —el catàleg, els socis, l'historial de préstecs— viu
únicament a la memòria del programa: en el moment en què el procés acaba, tot desapareix amb
ell. Aquesta lliçó obre el Mòdul 5 (Treballant amb Dades) atacant exactament aquest problema
pel camí més directe: llegir i escriure fitxers a disc amb l'espai de noms (namespace)
System.IO. Aprendràs a comprovar si un fitxer existeix, a llegir-lo i escriure'l d'un cop o
línia a línia, a alliberar correctament els recursos que obres, i a tractar els errors propis
de l'E/S (entrada/sortida). Al final de la lliçó, Biblioteca sabrà desar i recuperar el seu
catàleg complet en un fitxer de text pla, la primera forma —molt bàsica— de persistència real
de tot el curs.
Contingut
- L'espai de noms
System.IOi els dos nivells de treball amb fitxers - Comprovar l'existència d'un fitxer:
File.Exists - Tot de cop:
File.ReadAllText/File.WriteAllTextiFile.ReadAllLines/File.WriteAllLines - Fitxers grans:
StreamReaderiStreamWriter usingiIDisposable: alliberar recursos correctament- Tractament d'excepcions d'E/S:
IOExceptioniFileNotFoundException - Persistint el catàleg de BiblioTech en text pla
- L'espai de noms
System.IO i els dos nivells de treball amb fitxers
System.IO i els dos nivells de treball amb fitxersTot el que té a veure amb fitxers i directoris a .NET viu a l'espai de noms System.IO. A
dins hi conviuen dos nivells de treball, pensats per a necessitats diferents:
| Nivell | Classes principals | Quan fer-lo servir |
|---|---|---|
| Alt nivell, "tot de cop" | File (mètodes estàtics) |
Fitxers petits o mitjans que caben còmodament a memòria |
| Baix nivell, "a poc a poc" | StreamReader, StreamWriter |
Fitxers grans, o quan es vol processar línia a línia sense carregar-lo sencer |
Tots dos nivells acaben fent servir el mateix mecanisme per sota (un Stream que representa
el flux de bytes cap a o des del disc), però File t'estalvia la gestió manual del flux quan
la mida del fitxer no és un problema. Aquesta lliçó presenta primer File, per ser el punt
d'entrada més senzill, i després StreamReader/StreamWriter per als casos en què aquest
enfocament no n'hi ha prou.
- Comprovar l'existència d'un fitxer:
File.Exists
File.ExistsAbans de llegir un fitxer, convé comprovar que existeix; abans d'escriure'l, no sempre cal
(File.WriteAllText el crea si no existeix), però sí que és una bona pràctica abans de
qualsevol lectura:
string ruta = "catalog.txt";
if (File.Exists(ruta))
{
Console.WriteLine("El fitxer de cataleg ja existeix.");
}
else
{
Console.WriteLine("Encara no s'ha desat cap cataleg.");
}File.Exists(ruta) retorna true/false sense llançar cap excepció, fins i tot si la ruta té
un format invàlid o el directori no existeix (en aquests casos simplement retorna false). És
la manera segura d'anticipar-se a un fitxer absent, en lloc d'intentar llegir-lo directament i
capturar l'excepció que això provocaria.
- Tot de cop:
File.ReadAllText/File.WriteAllText i File.ReadAllLines/File.WriteAllLines
File.ReadAllText/File.WriteAllText i File.ReadAllLines/File.WriteAllLinesLa classe estàtica File ofereix quatre mètodes d'ús immediat per treballar amb el contingut
complet d'un fitxer:
| Mètode | Llegeix/escriu | Retorna/rep |
|---|---|---|
File.WriteAllText(ruta, contingut) |
Escriu | Un únic string (crea o sobreescriu el fitxer sencer) |
File.ReadAllText(ruta) |
Llegeix | Un únic string amb tot el contingut |
File.WriteAllLines(ruta, linies) |
Escriu | Un IEnumerable<string>, una línia per element |
File.ReadAllLines(ruta) |
Llegeix | Un string[], una posició per línia del fitxer |
// Escriure tot el contingut de cop
File.WriteAllText("salutacio.txt", "Benvingut a BiblioTech");
// Llegir-lo de tornada, tambe de cop
string contingut = File.ReadAllText("salutacio.txt");
Console.WriteLine(contingut); // "Benvingut a BiblioTech"
// Escriure diverses linies a partir d'un array o una llista
string[] titols = { "Rayuela", "Ficcions", "El Aleph" };
File.WriteAllLines("titols.txt", titols);
// Llegir-les de tornada, una per posicio de l'array
string[] titolsLlegits = File.ReadAllLines("titols.txt");
foreach (string titol in titolsLlegits)
{
Console.WriteLine(titol);
}Tots dos mètodes d'escriptura sobreescriuen el fitxer si ja existia (no afegeixen contingut
al final); si cal afegir sense esborrar l'anterior, hi ha les variants File.AppendAllText i
File.AppendAllLines, amb la mateixa forma d'ús. Aquests quatre mètodes són l'opció per
defecte sempre que el fitxer tingui una mida raonable: internament obren el fitxer, llegeixen
o escriuen tot el seu contingut, i el tanquen automàticament per tu, sense que hagis de
gestionar res més.
- Fitxers grans:
StreamReader i StreamWriter
StreamReader i StreamWriterQuan un fitxer és massa gran per carregar-lo sencer a memòria de cop (pensa en un .csv amb
milions de files), o quan vols començar a processar les seves línies abans que acabi de
llegir-se del tot, File.ReadAllText/File.ReadAllLines deixen de ser adequats: carreguen
tot el fitxer a memòria abans de retornar el control. StreamReader i StreamWriter
resolen això treballant línia a línia, sense necessitat de tenir el fitxer complet a
memòria en cap moment:
// Escriure linia a linia amb StreamWriter
StreamWriter escriptor = new StreamWriter("cataleg_gran.txt");
escriptor.WriteLine("Rayuela;Julio Cortazar");
escriptor.WriteLine("Ficcions;Jorge Luis Borges");
escriptor.Close(); // allibera el fitxer; sense aixo, les dades podrien no arribar a escriure's
// Llegir linia a linia amb StreamReader
StreamReader lector = new StreamReader("cataleg_gran.txt");
string? linia;
while ((linia = lector.ReadLine()) != null)
{
Console.WriteLine($"Linia llegida: {linia}");
}
lector.Close();ReadLine() retorna null quan arriba al final del fitxer, cosa que permet fer-lo servir
directament com a condició d'un while: el bucle es repeteix mentre hi hagi línies, i acaba
només quan ReadLine() retorna null. Aquest patró (while ((linia = lector.ReadLine()) != null)) és tan habitual en C# que convé memoritzar-lo tal qual.
L'exemple anterior té, a propòsit, un problema: si es produís una excepció entre l'obertura i
el Close(), el fitxer quedaria obert per sempre (una fuga de recursos, resource leak).
L'apartat següent resol exactament això.
using i IDisposable: alliberar recursos correctament
using i IDisposable: alliberar recursos correctamentStreamReader i StreamWriter (igual que SqliteConnection, que veuràs a la lliçó de
Connectivitat amb Bases de Dades) representen un recurs extern al programa —un fitxer obert
pel sistema operatiu— que s'ha d'alliberar explícitament quan s'acaba d'utilitzar. Tots
implementen la interfície IDisposable (recorda IDisposable de la lliçó de Constructors i
Destructors, Mòdul 3), el mètode Dispose() del qual allibera aquest recurs.
Cridar Dispose() (o el seu equivalent Close()) manualment, com a l'apartat anterior,
funciona, però obliga a recordar-se de fer-ho a tots els camins de sortida del mètode,
inclosos els que passen per una excepció. La sentència using automatitza això per complet:
using (StreamWriter escriptor = new StreamWriter("cataleg_gran.txt"))
{
escriptor.WriteLine("Rayuela;Julio Cortazar");
escriptor.WriteLine("Ficcions;Jorge Luis Borges");
} // aqui es crida automaticament escriptor.Dispose(), fins i tot si alguna cosa llanca una excepcio dins del bloc
using (StreamReader lector = new StreamReader("cataleg_gran.txt"))
{
string? linia;
while ((linia = lector.ReadLine()) != null)
{
Console.WriteLine($"Linia llegida: {linia}");
}
} // idem: lector.Dispose() es crida sempre en sortir del blocDes de C# 8 existeix a més la using declaration, sense claus pròpies, que allibera el
recurs al final del bloc que la conté (el mètode, o el bloc { } en què es va declarar):
using StreamReader lector = new StreamReader("cataleg_gran.txt");
string? linia;
while ((linia = lector.ReadLine()) != null)
{
Console.WriteLine(linia);
}
// lector.Dispose() es crida automaticament aqui, al final del metode (o del bloc contenidor)| Forma | Sintaxi | Quan s'allibera el recurs |
|---|---|---|
using (bloc) |
using (var x = ...) { ... } |
Al final de les claus { } |
using (declaració) |
using var x = ...; |
Al final del mètode o bloc contenidor |
Manual (Close()/Dispose()) |
Crida explícita | Quan el programador se'n recorda de cridar-lo —fràgil, evita-ho |
Regla pràctica per a la resta del curs: qualsevol objecte que implementi IDisposable
(fitxers, connexions a bases de dades, HttpClient...) es declara amb using, sense
excepcions, tret de casos molt concrets que queden fora de l'abast d'aquest curs.
- Tractament d'excepcions d'E/S:
IOException i FileNotFoundException
IOException i FileNotFoundExceptionQualsevol operació d'E/S pot fallar per motius aliens al programa: el fitxer no existeix, el
disc és ple, un altre procés el té bloquejat, no hi ha prou permisos... Totes aquestes
situacions es manifesten mitjançant excepcions, que es tracten amb try/catch, exactament
com a la lliçó de Gestió d'Excepcions (Mòdul 2):
| Excepció | Quan es llança |
|---|---|
FileNotFoundException |
S'intenta obrir per a lectura un fitxer que no existeix |
DirectoryNotFoundException |
El directori de la ruta indicada no existeix |
UnauthorizedAccessException |
No hi ha prou permisos per llegir o escriure aquesta ruta |
IOException |
Classe base d'errors d'E/S més generals (disc ple, fitxer bloquejat per un altre procés...) |
FileNotFoundException, DirectoryNotFoundException i UnauthorizedAccessException
hereten, de fet, d'IOException, així que un únic catch (IOException ex) captura
qualsevol d'elles si no necessites distingir-les; si sí que ho necessites, col·loca els
catch més específics primer, tal com vas aprendre a la lliçó d'excepcions:
try
{
string contingut = File.ReadAllText("cataleg.txt");
Console.WriteLine(contingut);
}
catch (FileNotFoundException)
{
Console.WriteLine("Encara no existeix cap cataleg desat.");
}
catch (IOException ex)
{
Console.WriteLine($"Error d'E/S en llegir el cataleg: {ex.Message}");
}A la pràctica, comprovar File.Exists per endavant evita la majoria de
FileNotFoundException; la resta d'excepcions d'IOException sí que convé capturar-les
sempre que el programa depengui que l'operació tingui èxit.
- Persistint el catàleg de BiblioTech en text pla
Amb tot l'anterior, Biblioteca guanya dos mètodes nous per desar i recuperar el seu catàleg
(Cataleg, de la lliçó de Col·leccions) en un fitxer de text pla: una línia per material, amb
els camps separats per ;. Com que el catàleg barreja Llibre i Revista (recorda la lliçó
d'Herència), cada línia comença amb un camp addicional que identifica el tipus:
class Biblioteca
{
// ... Cataleg, Socis, Prestecs, _socisPerId, PrestecRegistrat sense canvis ...
// ... AfegirMaterial, AfegirSoci, CercarSociPerId, RegistrarPrestec, PrestarLlibreAsync sense canvis ...
public void DesarCatalegText(string ruta)
{
using StreamWriter escriptor = new StreamWriter(ruta);
foreach (MaterialBibliotecari material in Cataleg)
{
if (material is Llibre llibre)
{
escriptor.WriteLine($"LLIBRE;{llibre.Titol};{llibre.Autor};{llibre.Isbn};{llibre.Disponible}");
}
else if (material is Revista revista)
{
escriptor.WriteLine($"REVISTA;{revista.Titol};{revista.Autor};{revista.NumeroEdicio};{revista.Disponible}");
}
}
}
public void CarregarCatalegText(string ruta)
{
if (!File.Exists(ruta))
{
throw new FileNotFoundException($"No s'ha trobat el fitxer de cataleg: {ruta}");
}
Cataleg.Clear();
using StreamReader lector = new StreamReader(ruta);
string? linia;
while ((linia = lector.ReadLine()) != null)
{
string[] camps = linia.Split(';');
string tipus = camps[0];
string titol = camps[1];
string autor = camps[2];
bool disponible = bool.Parse(camps[4]);
MaterialBibliotecari material = tipus switch
{
"LLIBRE" => new Llibre(titol, autor, camps[3]),
"REVISTA" => new Revista(titol, autor, int.Parse(camps[3])),
_ => throw new InvalidOperationException($"Tipus de material desconegut: {tipus}")
};
if (!disponible)
{
material.Prestar(); // deixa el material marcat com a no disponible, igual que estava en desar-lo
}
AfegirMaterial(material);
}
}
}Alguns detalls d'aquest parseig manual mereixen explicar-se:
linia.Split(';')(recordant Arrays i Cadenes de Text, Mòdul 1) trosseja cada línia en unstring[]fent servir;com a separador; l'ordre dels camps ha de coincidir exactament entreDesarCatalegText(en escriure) iCarregarCatalegText(en llegir).- L'
switchd'expressió (Pattern Matching, Mòdul 4) reconstrueix el tipus concret (LlibreoRevista) a partir del primer camp, reutilitzant els seus constructors tal com es van definir a la lliçó d'Herència. CarregarCatalegTextreutilitzaAfegirMaterial, ja existent aBiblioteca, en lloc de cridar directamentCataleg.Add(...): la lògica d'alta queda en un únic lloc.- Aquest format és deliberadament fràgil: si un títol contingués un
;, elSplitproduiria més camps dels esperats i el parseig fallaria. És exactament la limitació que motiva la lliçó següent, Serialització, amb un format (JSON) que no té aquest problema.
flowchart LR
A[Biblioteca.Cataleg en memoria] -->|DesarCatalegText| B[cataleg.txt]
B -->|CarregarCatalegText| C[Biblioteca.Cataleg reconstruit]
Errors Comuns i Consells
- Oblidar
usingen unStreamReader/StreamWriter: sense ell, el fitxer pot quedar bloquejat per a altres processos, o les dades escrites poden no arribar a desar-se a disc si el procés acaba abans de cridarClose()/Dispose()explícitament. Fes servir sempreusing, sense excepció, per a qualsevol tipus que implementiIDisposable. - Llegir un fitxer sense comprovar abans
File.Exists: intentarFile.ReadAllTextsobre un fitxer inexistent llançaFileNotFoundException; comprova-ho abans, o captura l'excepció si prefereixes l'enfocament "intenta-ho i captura" en lloc de "comprova abans". - Confondre
File.WriteAllText/WriteAllLinesamb "afegir al final": tots dos sobreescriuen el fitxer complet si ja existia; fes servirFile.AppendAllText/File.AppendAllLinessi la intenció és afegir contingut sense perdre l'anterior. - Un separador que pot aparèixer dins de les pròpies dades: el
;d'aquest format de text pla no està pensat per a títols que ja continguin un;; en un cas real caldria escapar el separador o triar un format més robust (el problema que resol la lliçó següent). - Consell: per a fitxers petits o mitjans (la immensa majoria de casos en una aplicació
típica),
File.ReadAllText/WriteAllText/ReadAllLines/WriteAllLinessón més simples i menys propensos a errors que gestionar unStreamReader/StreamWritera mà; reserva'ls per a fitxers realment grans o per processar línia a línia sense carregar-lo sencer.
Exercicis
-
Escriu un fragment de codi que faci servir
File.WriteAllLinesper desar tres títols de llibre en un fitxertitols.txt, i desprésFile.ReadAllLinesper llegir-los de tornada i mostrar-los per consola, un per línia. -
Escriu un mètode
void MostrarFitxerSiExisteix(string ruta)que comprovi ambFile.Existssi el fitxer existeix: si existeix, llegeix-lo ambFile.ReadAllTexti mostra'l per consola dins d'untry/catchque capturiIOException; si no existeix, mostra un missatge que ho indiqui, sense intentar llegir-lo. -
Construeix
DesarCatalegTextiCarregarCatalegTextaBibliotecatal com s'han presentat en aquesta lliçó. Crea unaBiblioteca, afegeix-hi dos llibres i una revista, desa el catàleg acataleg.txt, crea una segona instància deBibliotecabuida, carrega-hi el fitxer ambCarregarCatalegText, i comprova queCataleg.Countcoincideix entre totes dues instàncies.
Solucions
string[] titols = { "Rayuela", "Ficcions", "El Aleph" };
File.WriteAllLines("titols.txt", titols);
string[] titolsLlegits = File.ReadAllLines("titols.txt");
foreach (string titol in titolsLlegits)
{
Console.WriteLine(titol);
}
void MostrarFitxerSiExisteix(string ruta)
{
if (!File.Exists(ruta))
{
Console.WriteLine($"El fitxer '{ruta}' no existeix.");
return;
}
try
{
string contingut = File.ReadAllText(ruta);
Console.WriteLine(contingut);
}
catch (IOException ex)
{
Console.WriteLine($"Error d'E/S en llegir el fitxer: {ex.Message}");
}
}
Biblioteca origen = new Biblioteca();
origen.AfegirMaterial(new Llibre("Rayuela", "Julio Cortazar", "978-84-376-0495-4"));
origen.AfegirMaterial(new Llibre("Ficcions", "Jorge Luis Borges", "978-84-376-0496-1"));
origen.AfegirMaterial(new Revista("National Geographic", "Varios autores", 302));
origen.DesarCatalegText("cataleg.txt");
Biblioteca desti = new Biblioteca();
desti.CarregarCatalegText("cataleg.txt");
Console.WriteLine(origen.Cataleg.Count); // 3
Console.WriteLine(desti.Cataleg.Count); // 3
Conclusió
En aquesta lliçó has après a treballar amb fitxers fent servir System.IO: comprovar la seva
existència amb File.Exists, llegir i escriure de cop amb File.ReadAllText/WriteAllText i
ReadAllLines/WriteAllLines, processar fitxers grans línia a línia amb
StreamReader/StreamWriter, alliberar recursos correctament amb using, i tractar les
excepcions pròpies de l'E/S. Biblioteca ja pot desar i recuperar el seu catàleg complet entre
execucions, amb DesarCatalegText i CarregarCatalegText.
El format de text pla amb ; com a separador, però, és fràgil: no suporta bé títols amb
caràcters especials, obliga a un parseig manual propens a errors, i no escala bé cap a
estructures de dades més complexes (com els préstecs, que enllacen un llibre amb un soci). La
lliçó següent, Serialització, substitueix aquest parseig manual per System.Text.Json, un
format estàndard que resol aquests problemes d'arrel i que reutilitzarà, precisament, els
fitxers que ja saps llegir i escriure gràcies a aquesta lliçó.
Curs de Programació en C#
Mòdul 1: Introducció al C#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
