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

  1. L'espai de noms System.IO i els dos nivells de treball amb fitxers
  2. Comprovar l'existència d'un fitxer: File.Exists
  3. Tot de cop: File.ReadAllText/File.WriteAllText i File.ReadAllLines/File.WriteAllLines
  4. Fitxers grans: StreamReader i StreamWriter
  5. using i IDisposable: alliberar recursos correctament
  6. Tractament d'excepcions d'E/S: IOException i FileNotFoundException
  7. Persistint el catàleg de BiblioTech en text pla

  1. L'espai de noms System.IO i els dos nivells de treball amb fitxers

Tot 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.

  1. Comprovar l'existència d'un fitxer: File.Exists

Abans 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.

  1. Tot de cop: File.ReadAllText/File.WriteAllText i File.ReadAllLines/File.WriteAllLines

La 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.

  1. Fitxers grans: StreamReader i StreamWriter

Quan 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ò.

  1. using i IDisposable: alliberar recursos correctament

StreamReader 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 bloc

Des 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.

  1. Tractament d'excepcions d'E/S: IOException i FileNotFoundException

Qualsevol 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.

  1. 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 un string[] fent servir ; com a separador; l'ordre dels camps ha de coincidir exactament entre DesarCatalegText (en escriure) i CarregarCatalegText (en llegir).
  • L'switch d'expressió (Pattern Matching, Mòdul 4) reconstrueix el tipus concret (Llibre o Revista) a partir del primer camp, reutilitzant els seus constructors tal com es van definir a la lliçó d'Herència.
  • CarregarCatalegText reutilitza AfegirMaterial, ja existent a Biblioteca, en lloc de cridar directament Cataleg.Add(...): la lògica d'alta queda en un únic lloc.
  • Aquest format és deliberadament fràgil: si un títol contingués un ;, el Split produiria 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 using en un StreamReader/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 cridar Close()/Dispose() explícitament. Fes servir sempre using, sense excepció, per a qualsevol tipus que implementi IDisposable.
  • Llegir un fitxer sense comprovar abans File.Exists: intentar File.ReadAllText sobre un fitxer inexistent llança FileNotFoundException; comprova-ho abans, o captura l'excepció si prefereixes l'enfocament "intenta-ho i captura" en lloc de "comprova abans".
  • Confondre File.WriteAllText/WriteAllLines amb "afegir al final": tots dos sobreescriuen el fitxer complet si ja existia; fes servir File.AppendAllText/ File.AppendAllLines si 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/WriteAllLines són més simples i menys propensos a errors que gestionar un StreamReader/StreamWriter a mà; reserva'ls per a fitxers realment grans o per processar línia a línia sense carregar-lo sencer.

Exercicis

  1. Escriu un fragment de codi que faci servir File.WriteAllLines per desar tres títols de llibre en un fitxer titols.txt, i després File.ReadAllLines per llegir-los de tornada i mostrar-los per consola, un per línia.

  2. Escriu un mètode void MostrarFitxerSiExisteix(string ruta) que comprovi amb File.Exists si el fitxer existeix: si existeix, llegeix-lo amb File.ReadAllText i mostra'l per consola dins d'un try/catch que capturi IOException; si no existeix, mostra un missatge que ho indiqui, sense intentar llegir-lo.

  3. Construeix DesarCatalegText i CarregarCatalegText a Biblioteca tal com s'han presentat en aquesta lliçó. Crea una Biblioteca, afegeix-hi dos llibres i una revista, desa el catàleg a cataleg.txt, crea una segona instància de Biblioteca buida, carrega-hi el fitxer amb CarregarCatalegText, i comprova que Cataleg.Count coincideix 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#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats