LINQ: interrogare i dati in C#
LINQ, acronimo di Language Integrated Query, è una delle funzionalità che rendono C# particolarmente espressivo. Permette di interrogare, filtrare, trasformare e aggregare collezioni di dati con una sintassi dichiarativa e leggibile, applicando lo stesso modello indipendentemente dalla sorgente: liste in memoria, database o documenti XML. Poggiando su generics, delegati ed espressioni lambda, LINQ mette a frutto tutto ciò che abbiamo visto finora. In questo articolo ne esploriamo le basi e gli operatori più importanti.
Due sintassi equivalenti
LINQ offre due stili intercambiabili. La sintassi di query, simile a un linguaggio di interrogazione, e la sintassi a metodi, che concatena chiamate a metodi di estensione. Entrambe producono lo stesso risultato; la sintassi a metodi è la più diffusa per la sua flessibilità.
using System.Linq;
var numbers = new List<int> { 5, 3, 8, 1, 9, 2 };
// Sintassi di query
var querySyntax = from n in numbers
where n > 3
orderby n
select n;
// Sintassi a metodi, equivalente
var methodSyntax = numbers
.Where(n => n > 3)
.OrderBy(n => n);
Filtrare con Where
L'operatore Where seleziona gli elementi che soddisfano una condizione, espressa come lambda che restituisce un booleano. È l'equivalente di un filtro applicato alla sequenza.
var words = new List<string> { "casa", "albero", "sole", "montagna" };
// Solo le parole con piu di quattro caratteri
var longWords = words.Where(w => w.Length > 4);
// Contiene "albero" e "montagna"
Trasformare con Select
L'operatore Select proietta ogni elemento in una nuova forma, applicando una trasformazione. Il risultato è una sequenza dello stesso numero di elementi, ma potenzialmente di un tipo diverso.
var numbers = new List<int> { 1, 2, 3, 4 };
// Trasforma ogni numero nel suo quadrato
var squares = numbers.Select(n => n * n);
// Contiene 1, 4, 9, 16
// Puo produrre un tipo diverso
var descriptions = numbers.Select(n => $"Numero {n}");
Ordinare i risultati
Per ordinare una sequenza si usano OrderBy e OrderByDescending, indicando la chiave di ordinamento. Per criteri secondari si concatenano ThenBy e ThenByDescending.
var people = new List<(string Name, int Age)>
{
("Anna", 30),
("Marco", 25),
("Eva", 30)
};
// Ordina per eta decrescente, poi per nome crescente
var ordered = people
.OrderByDescending(p => p.Age)
.ThenBy(p => p.Name);
Aggregare e contare
LINQ fornisce numerosi operatori che riducono una sequenza a un singolo valore. Tra i più usati ci sono Count, Sum, Average, Min e Max, che spesso accettano una lambda per selezionare il valore su cui operare.
var prices = new List<decimal> { 9.99m, 19.99m, 4.99m, 29.99m };
int howMany = prices.Count(); // 4
decimal total = prices.Sum(); // somma di tutti i prezzi
decimal average = prices.Average(); // media
decimal highest = prices.Max(); // valore massimo
// Con una condizione: quanti superano i 10 euro
int expensive = prices.Count(p => p > 10m);
Selezionare singoli elementi
Un altro gruppo di operatori recupera un singolo elemento dalla sequenza. È importante conoscere la differenza tra le varianti: First solleva un'eccezione se non trova nulla, mentre FirstOrDefault restituisce il valore predefinito del tipo, offrendo un comportamento più sicuro.
var numbers = new List<int> { 4, 8, 15, 16, 23 };
int first = numbers.First(); // primo elemento
int firstBig = numbers.First(n => n > 10); // primo maggiore di 10
int found = numbers.FirstOrDefault(n => n > 100); // 0, nessuna corrispondenza
bool hasLarge = numbers.Any(n => n > 20); // true
bool allPositive = numbers.All(n => n > 0); // true
Raggruppare i dati
L'operatore GroupBy organizza gli elementi in gruppi in base a una chiave, restituendo una sequenza di gruppi ciascuno identificato dalla propria chiave. È molto utile per produrre riepiloghi e statistiche.
var products = new List<(string Name, string Category)>
{
("Mela", "Frutta"),
("Carota", "Verdura"),
("Pera", "Frutta"),
("Zucchina", "Verdura")
};
// Raggruppa per categoria e conta gli elementi di ciascun gruppo
var byCategory = products
.GroupBy(p => p.Category)
.Select(g => new { Category = g.Key, Count = g.Count() });
foreach (var group in byCategory)
{
Console.WriteLine($"{group.Category}: {group.Count}");
}
L'esecuzione differita
Un concetto cruciale è che molte query LINQ non vengono eseguite immediatamente, ma solo quando i risultati vengono effettivamente richiesti, ad esempio iterando la sequenza o chiamando un metodo di materializzazione. Questa esecuzione differita consente di comporre query in più passaggi senza costi finché non servono i dati.
var numbers = new List<int> { 1, 2, 3 };
// La query e definita ma non ancora eseguita
var query = numbers.Where(n => n > 1);
numbers.Add(4); // modifica prima dell'esecuzione
// L'esecuzione avviene ora, includendo il 4 appena aggiunto
foreach (int n in query)
{
Console.WriteLine(n); // Stampa 2, 3, 4
}
Per forzare l'esecuzione immediata e ottenere una collezione concreta si usano metodi come ToList, ToArray o ToDictionary, che materializzano i risultati in quel momento.
Concatenare le operazioni
La vera eleganza di LINQ sta nella possibilità di combinare più operatori in una pipeline che si legge quasi come una descrizione in linguaggio naturale del risultato desiderato.
var orders = new List<(string Customer, decimal Amount)>
{
("Anna", 120m), ("Marco", 45m), ("Anna", 80m), ("Eva", 200m)
};
// Clienti con spesa totale superiore a 100, ordinati per spesa
var topCustomers = orders
.GroupBy(o => o.Customer)
.Select(g => new { Customer = g.Key, Total = g.Sum(o => o.Amount) })
.Where(x => x.Total > 100m)
.OrderByDescending(x => x.Total)
.ToList();
Conclusione
Abbiamo esplorato LINQ come strumento dichiarativo per lavorare sui dati, imparando a filtrare con Where, trasformare con Select, ordinare, aggregare, selezionare singoli elementi e raggruppare con GroupBy. Abbiamo compreso l'esecuzione differita e come materializzare i risultati, e visto come concatenare gli operatori in pipeline espressive. Nel prossimo articolo affronteremo la programmazione asincrona con async e await, essenziale per applicazioni reattive ed efficienti.