Amb Biblioteca ja reunint tot el catàleg i tots els socis en col·leccions, la pregunta natural
és: com es respon a preguntes com "quins llibres estan disponibles?", "quants materials hi ha de
cada autor?" o "quins préstecs porten més de dues setmanes sense retornar-se?" Sense més eines
que un foreach, cada pregunta exigiria escriure un bucle manual amb variables auxiliars.
LINQ (Language Integrated Query, Consulta Integrada en el Llenguatge) resol això amb un
conjunt d'operadors —filtrar, transformar, ordenar, agrupar— que es combinen de forma expressiva
i funcionen sobre qualsevol IEnumerable<T> (la interfície que tancava la lliçó anterior).
Aquesta lliçó t'ensenya a consultar el catàleg i els préstecs de BiblioTech amb LINQ, deixant
enrere els bucles manuals per a aquest tipus de tasques.
Contingut
- Què és LINQ i quin problema resol
- Sintaxi de mètode:
Where,Select,OrderBy - Agrupant resultats:
GroupBy - Operadors que retornen un únic valor:
FirstOrDefault,Any,Count - Sintaxi de consulta:
from...where...select - Execució diferida enfront d'execució immediata
- Consultant el catàleg i els préstecs de BiblioTech
- Què és LINQ i quin problema resol
LINQ afegeix, a qualsevol col·lecció que implementi IEnumerable<T> (List<T>, arrays,
Dictionary<TKey, TValue>.Values, i moltes més), un conjunt de mètodes d'extensió per
consultar-la de forma declarativa: en lloc de descriure com recórrer-la pas a pas amb un
foreach, descrius què resultat vols obtenir, i LINQ s'encarrega del recorregut per tu.
// Sense LINQ: descriure "com" recorrer, amb un foreach i una llista auxiliar
List<MaterialBibliotecari> disponibles = new List<MaterialBibliotecari>();
foreach (MaterialBibliotecari material in biblioteca.Cataleg)
{
if (material.Disponible)
{
disponibles.Add(material);
}
}
// Amb LINQ: descriure "que" resultat es vol
List<MaterialBibliotecari> disponiblesLinq = biblioteca.Cataleg.Where(m => m.Disponible).ToList();Tots dos fragments produeixen el mateix resultat, però el segon expressa directament la intenció
("els materials on Disponible és cert"), sense la mecànica intermèdia del bucle i la llista
auxiliar.
- Sintaxi de mètode:
Where, Select, OrderBy
Where, Select, OrderByEls operadors LINQ més usats, en la seva forma habitual (sintaxi de mètode, encadenant
crides amb .):
| Operador | Què fa | Exemple |
|---|---|---|
Where |
Filtra elements segons una condició (Func<T, bool>) |
cataleg.Where(m => m.Disponible) |
Select |
Transforma cada element en una altra cosa (projecció) | cataleg.Select(m => m.Titol) |
OrderBy |
Ordena de forma ascendent segons una clau | cataleg.OrderBy(m => m.Titol) |
OrderByDescending |
Ordena de forma descendent | cataleg.OrderByDescending(m => m.Titol) |
List<MaterialBibliotecari> disponibles = biblioteca.Cataleg
.Where(m => m.Disponible)
.OrderBy(m => m.Titol)
.ToList();
foreach (MaterialBibliotecari material in disponibles)
{
Console.WriteLine(material.Titol);
}Aquest encadenat es llegeix d'esquerra a dreta com una canonada: "agafa el catàleg, queda't
només amb els disponibles, ordena'ls per títol, i converteix-ho en una llista". Select
transforma cada element, útil quan no necessites l'objecte complet sinó només una de les seves
dades:
List<string> titols = biblioteca.Cataleg.Select(m => m.Titol).ToList();
// ["Rayuela", "Ficcions", "National Geographic"]
- Agrupant resultats:
GroupBy
GroupByGroupBy reparteix els elements d'una col·lecció en grups segons una clau, cosa que amb un
foreach manual requeriria un Dictionary<TClau, List<T>> construït a mà:
var perAutor = biblioteca.Cataleg.GroupBy(m => m.Autor);
foreach (var grup in perAutor)
{
Console.WriteLine($"{grup.Key}: {grup.Count()} material(s)");
foreach (MaterialBibliotecari material in grup)
{
Console.WriteLine($" - {material.Titol}");
}
}Cada grup és, alhora, la clau (grup.Key, l'autor) i una col·lecció de tots els elements amb
aquesta clau, sobre la qual es pot tornar a iterar o aplicar més LINQ. var es fa servir aquí
perquè el tipus exacte que retorna GroupBy (IEnumerable<IGrouping<string, MaterialBibliotecari>>) és llarg d'escriure i rarament cal anomenar-lo explícitament.
- Operadors que retornen un únic valor:
FirstOrDefault, Any, Count
FirstOrDefault, Any, CountNo totes les consultes retornen una col·lecció; algunes responen amb un únic valor:
| Operador | Què retorna |
|---|---|
FirstOrDefault(condicio) |
El primer element que compleix la condició, o default (normalment null) si cap la compleix |
First(condicio) |
Igual, però llança una excepció si cap element la compleix |
Any(condicio) |
true si almenys un element compleix la condició |
Count(condicio) |
Quants elements compleixen la condició |
Count() (sense argument) |
El nombre total d'elements |
MaterialBibliotecari? primerDisponible = biblioteca.Cataleg.FirstOrDefault(m => m.Disponible);
bool hiHaRevistes = biblioteca.Cataleg.Any(m => m is Revista);
int totalLlibres = biblioteca.Cataleg.Count(m => m is Llibre);
Console.WriteLine(primerDisponible?.Titol ?? "Cap disponible");
Console.WriteLine(hiHaRevistes); // True
Console.WriteLine(totalLlibres); // 2FirstOrDefault és preferible a First sempre que "no trobar res" sigui una situació normal i
esperable (com aquí); reserva First per a quan l'absència de resultat representaria, en
realitat, un error en la lògica del programa.
- Sintaxi de consulta:
from...where...select
from...where...selectA més de la sintaxi de mètode, C# ofereix una sintaxi de consulta, més semblant a SQL, que
el compilador tradueix internament als mateixos mètodes (Where, Select...) vistos abans:
var disponiblesQuery =
from material in biblioteca.Cataleg
where material.Disponible
orderby material.Titol
select material;
foreach (MaterialBibliotecari material in disponiblesQuery)
{
Console.WriteLine(material.Titol);
}Aquesta consulta és exactament equivalent a
biblioteca.Cataleg.Where(m => m.Disponible).OrderBy(m => m.Titol): dues maneres diferents
d'escriure la mateixa operació. A la pràctica, la sintaxi de mètode és la més habitual en codi
C# modern (encaixa millor amb encadenats llargs i amb operadors que no tenen equivalent directe
en la sintaxi de consulta, com Count o Any); la sintaxi de consulta es fa servir sobretot
quan hi ha diverses fonts combinades (join) o quan l'equip la prefereix per llegibilitat. N'hi
ha prou de reconèixer totes dues i saber que són intercanviables.
- Execució diferida enfront d'execució immediata
Un detall important, fàcil de passar per alt: la majoria d'operadors LINQ (Where, Select,
OrderBy...) no s'executen en el moment en què s'escriuen, sinó quan es recorre el resultat
(amb foreach, o en cridar ToList()/ToArray()/Count()). Això s'anomena execució
diferida (deferred execution):
var consulta = biblioteca.Cataleg.Where(m => m.Disponible); // encara no s'ha avaluat res
biblioteca.AfegirMaterial(new Llibre("El Aleph", "Jorge Luis Borges", "978-84-376-0497-8"));
foreach (MaterialBibliotecari material in consulta) // AQUI s'executa el filtre, sobre el cataleg ja actualitzat
{
Console.WriteLine(material.Titol); // inclou tambe "El Aleph", tot i que es va afegir despres de "consulta"
}Operadors com ToList(), ToArray(), Count() o FirstOrDefault() forcen, en canvi, una
execució immediata: el resultat es calcula en aquell moment i queda fixat, sense veure's
afectat per canvis posteriors en la col·lecció d'origen. Per a ús quotidià n'hi ha prou de
recordar aquesta regla pràctica: si recorreràs el mateix resultat diverses vegades, o necessites
"congelar-lo" en un moment concret, afegeix .ToList() al final de la consulta.
- Consultant el catàleg i els préstecs de BiblioTech
Amb tot l'anterior, així es responen les preguntes plantejades al principi de la lliçó sobre
Biblioteca:
// Quins llibres estan disponibles, ordenats per titol?
List<MaterialBibliotecari> llibresDisponibles = biblioteca.Cataleg
.Where(m => m is Llibre && m.Disponible)
.OrderBy(m => m.Titol)
.ToList();
// Quants materials hi ha de cada autor?
var materialsPerAutor = biblioteca.Cataleg
.GroupBy(m => m.Autor)
.Select(g => new { Autor = g.Key, Quantitat = g.Count() });
foreach (var entrada in materialsPerAutor)
{
Console.WriteLine($"{entrada.Autor}: {entrada.Quantitat}");
}
// Quins prestecs porten mes de 14 dies sense retornar-se (vencuts)?
const int DiesLimitPrestec = 14;
List<Prestec> prestecsVencuts = biblioteca.Prestecs
.Where(p => p.DataDevolucio == null && (DateTime.Now - p.DataPrestec).Days > DiesLimitPrestec)
.ToList();
foreach (Prestec prestec in prestecsVencuts)
{
Console.WriteLine($"Vencut: '{prestec.Llibre.Titol}' prestat a {prestec.Soci.Nom} el {prestec.DataPrestec:yyyy-MM-dd}");
}materialsPerAutor fa servir una característica que potser encara no havies vist: new { Autor = g.Key, Quantitat = g.Count() } crea un tipus anònim, un objecte d'un sol ús amb només les
propietats que necessites mostrar, sense declarar una classe ni un record a part —útil
precisament per a resultats intermedis d'una consulta LINQ que no necessiten un tipus amb nom
propi a la resta del programa.
La consulta de préstecs vencuts combina dues condicions amb &&: que encara no s'hagi retornat
(DataDevolucio == null) i que hagin passat més dies que el límit de préstec des de
DataPrestec. És exactament el tipus de pregunta de negoci ("quins préstecs requereixen un avís
de retard?") que LINQ permet expressar en poques línies, sense cap bucle manual.
Errors Comuns i Consells
- Oblidar
.ToList()(o similar) quan cal una execució immediata: si modifiques la col·lecció d'origen entre la definició de la consulta i el seu recorregut, el resultat pot canviar de forma inesperada per l'execució diferida; fes servir.ToList()si necessites "fixar" el resultat en un moment concret. - Encadenar massa operadors en una sola línia il·legible: encara que tècnicament vàlid, una
cadena llarguíssima de
.Where().Select().OrderBy().GroupBy()...en una sola línia és difícil de llegir; divideix la consulta en diverses línies, una operació per línia, com als exemples d'aquesta lliçó. - Fer servir
Firstquan "no trobat" és una situació normal:FirstllançaInvalidOperationExceptionsi cap element compleix la condició; fes servirFirstOrDefaulti comprovais not null(recordant la lliçó de Pattern Matching) tret que l'absència de resultat sigui, en efecte, un error de programa. - Confondre
Count(operador LINQ) ambCount(propietat deList<T>):llista.Count(sense parèntesis) és una propietat ràpida deList<T>;llista.Count(condicio)(amb parèntesis i un predicat) és l'operador LINQ, que recorre la col·lecció per comptar quants elements compleixen la condició. Totes dues coexisteixen sense conflicte perquè tenen signatures diferents. - Consell: per a tasques de filtratge, transformació i ordre simples, la sintaxi de mètode és
més habitual i més fàcil de depurar pas a pas (pots comentar una línia de la cadena per veure
el resultat intermedi); reserva la sintaxi de consulta per quan combinis diverses fonts amb
joino quan l'equip la prefereixi explícitament.
Exercicis
-
Sobre
biblioteca.Cataleg, fes servir sintaxi de mètode per obtenir unaList<string>amb els títols de tots els materialsDisponible, ordenats alfabèticament. -
Sobre
biblioteca.Cataleg, fes servirGroupByper agrupar els materials per autor i mostra, per a cada autor, quants materials té al catàleg. -
Sobre
biblioteca.Prestecs, escriu una consulta que retorni els préstecs sense retornar (DataDevolucio == null) que porten més de 14 dies des deDataPrestec. Prova-ho amb almenys un préstec que compleixi la condició i un altre que no.
Solucions
List<string> titolsDisponibles = biblioteca.Cataleg
.Where(m => m.Disponible)
.OrderBy(m => m.Titol)
.Select(m => m.Titol)
.ToList();
foreach (string titol in titolsDisponibles)
{
Console.WriteLine(titol);
}
var perAutor = biblioteca.Cataleg.GroupBy(m => m.Autor);
foreach (var grup in perAutor)
{
Console.WriteLine($"{grup.Key}: {grup.Count()} material(s)");
}
const int DiesLimitPrestec = 14;
List<Prestec> vencuts = biblioteca.Prestecs
.Where(p => p.DataDevolucio == null && (DateTime.Now - p.DataPrestec).Days > DiesLimitPrestec)
.ToList();
foreach (Prestec prestec in vencuts)
{
Console.WriteLine($"Vencut: '{prestec.Llibre.Titol}' ({prestec.Soci.Nom})");
}
Per comprovar el resultat, es pot crear un Prestec amb una DataPrestec simulada més
antiga manualment (recordant que, en el model actual, DataPrestec es fixa automàticament a
DateTime.Now al constructor), o simplement raonar sobre préstecs acabats de crear, que mai
apareixeran com a vencuts en el mateix instant d'execució.
Conclusió
En aquesta lliçó has après a consultar col·leccions amb LINQ: filtrar amb Where, transformar
amb Select, ordenar amb OrderBy, agrupar amb GroupBy, i obtenir valors únics amb
FirstOrDefault, Any i Count; també has vist la sintaxi de consulta alternativa i la
diferència entre execució diferida i immediata. Biblioteca.Cataleg i Biblioteca.Prestecs ja
es poden consultar amb la mateixa expressivitat que una base de dades, sense haver escrit ni una
sola línia de SQL.
De fet, aquesta comparació no és casual: al Mòdul 5 (Treballant amb Dades), Entity Framework farà
servir LINQ com el seu llenguatge de consulta principal contra una base de dades real, així que
tot el que has après aquí es reutilitzarà directament. Abans d'arribar-hi, queda una última peça
en aquest mòdul: tot el que s'ha vist fins ara ha estat codi síncron, que bloqueja
l'execució mentre es completa. L'última lliçó del mòdul, Programació Asíncrona, introdueix
async/await i una primera versió simulada de PrestarLlibreAsync, preparant Biblioteca per
a les operacions d'E/S reals (fitxers, bases de dades) que arribaran al proper mòdul.
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
