C#Programmazione

Language INtegrated Query

LINQ fornisce uno strato di astrazione che rendono possibile l’accesso a diverse sorgente dati, senza preoccuparsi dei dettagli implementativi di ognuna di esse. Argomenti descritti precedentemente (variabili implicite, tipi anonimi, espressioni lamda) saranno la base fondamentale nella creazione ed esecuzione delle query LINQ.

Le espressioni sono scritte con un’apposita sintassi di query dichiarativa (query syntax), oppure mediante una serie di metodi di estensioni o operatori di query standard (method syntax). Le due sintassi sono combinabili in una singola query.

Ecco un esempio di query syntax che estrae il quadrato di tutti i numeri pari presenti in un array

IEnumerable<int> query = from n in array where n % 2 == 0 select n * n;

Ora vediamo lo stesso esempio ma con il method syntax abc, in questo esempio utilizziamo la dichiarazione di tipo implicito

var query = array.Where(n => n % 2 == 0).Select(n => n * n);

Da questi esempi si capisce che queste sono semplici dichiarazioni di quello che la query dovrà fare. Gli operatori di query sono implementati tramite i metodi di estensione delle classi statiche Enumerable e IQueryable.

La sintassi completa di una query LINQ ha il seguente formato

from [tipo] identificatore in espressione [clausole-query-body] clausola select | clausola group [continuazione-query]

Supponiamo di avere dei dati relativi ai teams di formula uno, una banale query che ci restituisce tutti gli ogetti è

List<F1Team> teams = F1Data.GetTeams();
var query = from team in teams select team;

Questo esempio di query eseguito su un oggetto IEnumerable non tipizzato

ArrayList list = new ArrayList();
list.AddRange(teams);
var query = from F1Team team in list select team.TeamName;

Una query che estrae solo il nome di tutti i teams è

var query = teams.Select(team => team.TeamName);

Scriviamo una query che estrae i cognomi dei piloti che fanno parte di un team con almeno due piloti e che il cognome sia più lungo di 8 caratteri

var query = from team in teams
from pilots in team.Pilots
where pilot.LastName.Lenght > 8
where team.Pilots.Lenght > 1
select pilot.LastName;

Dentro una query potremmo aver bisogno di un risultato di una sottoespressione, la parola chiave let permette di creare una variabile di range e inizializzarla per poi essere utilizzata.

Supponiamo di voler estrarre i teams ordinati con le vittorie vinte

var query = from team in teams
let wins = team.Wins
ordered wins ascending
where wins > 3
select team.TeamName + “:” + wins;

Supponiamo di voler estrarre tutti i tems, ordine decrescente per punti, che hanno un pilota con più di 50 punti

var query = from team in teams
let wins = team.Wins
let pilots = team.Pilots
from pilot in pilots
where pilot.Point > 50
orderby wins descending
select team.TeamName + ” wins:” + wins + “, leader ” + pilot.LastName + ” ” + pilot.Point;

Da questi esempi possiamo vedere che le query permettono di creare oggetti e memorizzare all’interno il risultato della query stessa. Ad esempio possiamo creare un oggetto Tuple formato dal nome della squadra e il numero di vittorie

var query = from team in teams
select new Tuple<string, int>(team.TeamName, team.Wins);
foreach (Tuple<string, int> t in query) Console.WriteLine($”{t.Item1}: {t.Item2}”);

Una seconda opzione è istanziare un oggetto anonimo impostando campi e proprietà pubbliche

var query = from team in teams
select new { TeamName = team.TeamName, Wins = team.Wins };
foreach (var t in query) Console.WriteLine($”{t.TeamName}: {t.Wins}”);

Possiamo realizzare la stessa cosa utilizzando l’espressioni lambda

var query = teams.Select(team => new { team.TeamName, team.Wins });

Immaginiamo di voler enumerare una sequenza a partire da ogni elemento di un’altra sequenza, e quindi ottenere un’unica collezione dall’unione delle due. Possiamo realizzare il tutto mediante clausole from composte oppure con l’operatore SelectMany

var query1 = from team in teams
let pilots = team.Pilots
from pilot in pilots
where pilot.LastName.Length > 7
select new { TeamName = team.TeamName, PilotName = pilot.LastName };


oppure

var query = teams.SelectMany(team => team.Pilots.Where(pilot => pilot.LastName.Length > 7), (team, pilot) => team.TeamName + ” – ” + pilot.LastName);

Dagli esempi visti notiamo che lo scopo delle ‘interrogazioni è quello restringere il numero di elementi della sorgente in base a filtri. Non tutti i filtri possono essere espressi mediante sintassi query, in casi più avanzati bisognerà ricorrere ai metodi di estensione che permettono di usare delegate ed espressioni lambda.

Possiamo ordinare i risultati, tramite una o più chiavi specificate, ottenuti tramite orderby, il valore predefinito è quello crescente (ascending) oppure specificare decrescente tramite descending. Possiamo ordinare i risultati tramite i metodi ThenBy e ThenByDescending. Mentre il metodo Reverse consente di invertire il risultato.

Possiamo decidere di raggruppare gli elementi di una sequenza secondo una particolare chiave tramite la clausola group … by. Possiamo estrarre tutti i piloti raggruppati per team

var query = from team in teams
from pilot in team.Pilots
group pilot by team;

L’interfaccia dell’ogetto gruppo è IGrouping<K, V>, dove K rappresenta la chiave mentre V è il tipo degli elementi contenuti. Nel nostro esempio possiamo visualizzare i risultati

foreach (IGrouping<F1Team, Pilot> group in query)
{
F1Team team = group.Key;
Console.WriteLine ($”Team: {team}”);
foreach (Pilot pilot in group)
{ Console.WriteLine ($”- {pilot.FirstName} {pilot.LastName}”); }
}

Il risultato di una query può essere passata ad una successiva mediante la clausola into. Supponiamo di voler estrarre tutti i piloti che fanno parte di un team con almeno due vittorie, di questi piloti ne straiamo solo quelli che hanno almeno 50 punti

var query = from team in teams
where team.Wins > 2
select team.Pilots
into topTeamPilots
from tp in topTeamPilots
where tp.Point > 50
orderby tp.Point descending
select tp.LastName + ” ” + tp.FirstName + “: ” + tp.Point;

Tramite la clausola join possiamo creare una sequenza a partire da due collezioni, accoppiando un elemento della prima con uno della seconda. Nell’esempio possiamo estrarre per ogni pilota il nome della relativa nazione

var query = from team in teams
from pilot in team.Pilots
join country in Country.All on pilot.IDCountry equals country.IDCountry
select new { pilot.LastName, CountryName = country.Name };

oppure

var query = teams.SelectMany(t => t.Pilots).Join(Country.All, pilot => pilot.IDCountry, country => country.IDCountry,
(p, c) => new {p.LastName, CountryName = c.Name});

Vediamo ora come utilizzare due join, il secondo tramite la clausola into, oppure il metodo joingroup. Proviamo ad estrarre una collezione di nazioni, a ciacuna delle quali corrisponde una collezione di piloti.

var pilots = from team in teams orderby team.TeamName
from pilot in team.Pilots
select pilot;

var query = from country in Country.All
join pilot in pilots on country.IDCountry equals pilot.IDCountry
into pilotsxCountry
select new { CountryName = country.Name, Pilots = pilotsxCountry };

oppure

IEnumerable pilots = teams.SelectMany(t => t.Pilots);
var query = Country.All.GroupJoin(pilots, country => country.IDCountry, pilot => pilot.IDCountry, (c, p) => new {CountryName = c.Name, Pilots = p});
foreach (var group in query)
{
Console.WriteLine(group.CountryName);
foreach (Pilot pilot in group.Pilots)
{
Console.WriteLine($” – {pilot.LastName}”);
}
}

Possiamo gestire i dati come insieme di elementi e dalla loro elaborazione ottenere i risultati richiesti, in questi esempi usiamo

  • Distinct – restituisce gli elementi presenti nell’insiemi eliminando i duplicati;
  • Union – unisce due insiemi eliminando le duplicazioni;
  • Except – restituisce gli elementi del primo insieme che non sono presenti nel secondo;
  • Intersect – restituisce gli elementi che fanno parte dei due insiemi;
  • Zip – restituisce un nuovo insieme applicando una funzione ad ogni coppia di elementi;
  • Skip – restituisce gli elementi saltando i primi elementi indicati;
  • SkipWhile – restituisce gli elementi a partire da una determinata condizione;
  • Take – restituisce i primi elementi indicati;
  • TakeWhile – restituisce i primi elementi fino ad una determinata condizione;
var qDist = array1.Distinct();
var qUni = array1.Union(array2);
var qExc = array1.Except(array2);
var qInt = array1.Intersect(array2);
var qZip = array1.Zip(array2, (a,b) => a*10+b);
var qSkip = array1.Skip(4);
var qSWhile = array1.SkipWhile(n => n<5);
var qTake = array2.Take(4);
var qTakeWhile = array1.TakeWhile(n => n<3);

Tramite gli operatori quantificatori possiamo verificare l’esistenza di elementi:

  • Contains – verifica se la sequenza contiene un particolare elemento;
  • All – verifica se tutti gli elementi di una sequenza soddisfano una condizione;
  • Any – verifica che almeno un elemento soddisfa la condizione;

Possiamo combinare diversi metodi per creare query articolate; il primo esempio restituisce i team che hanno i loro piloti con punteggio maggiore di 18, mentre il secondo i team che hanno almeno un pilota con un punteggio maggiore di 70.

var query = teams.Where(t => t.Pilots.All(p => p.Point>18));

var query = teams.Where(t => t.Pilots.Any(p => p.Point > 70));

Possiamo recuperare il primo elemento con First, con Last recuperiamo l’ultimo elemento mentre Single recupera l’unico elemento. Se vogliamo ricevere eventualmente un valore di default usiamo ElementAtOrDefault, FirstAtOrDefault, LastAtOrDefault, SingleAtOrDefault.

Possiamo contare gli elementi di una sequenza tramite Count e LongCount, tutti senza nessun parametro oppure quelli che soddisfano la condizione. Possiamo fare la somma dei valori di una sequenza, ad esempio possiamo per ogni team sommare i punti dei relativi piloti

 

var query = from team in teams
let points = team.Pilots.Sum(p => p.Point)
orderby points descending
select new { team.TeamName, points };

Possiamo calcolare la media tramite Average, oppure il minimo Min o il massimo Max di una sequenza. Tramite MinBy e MaxBy possiamo ottenere un oggetto complesso da una sequenza. In questo esempio otteniamo i piloti al primo e all’ultimo posto

var primo = (from team in teams
from pilot in team.Pilots
select pilot).MaxBy(p => p.Point);
var ultimo = (from team in teams
from pilot in team.Pilots
select pilot).MinBy(p => p.Point);

Aggregate consente di specificare un’operazione personalizzata di accumulo, ad esempio possiamo ottenere per ogni team la somma dei punti di tutti i piloti di un team

var query = from team in teams
select new { Name = team.TeamName, Points = team.Pilots.Aggregate(0, (total, p) => total + p.Point) };

Gli operatori ToArray, ToList, ToDictionary e ToLookup permettono di effettuare una conversione di tipo.

  • ToList – una lista di team che iniziano per M
    List<F1Team> result = teams.Where(t => t.TeamName.ToUpper().StartsWith(“M”)).ToList();
  • ToArray – un array di piloti
    Pilot[] array = teams.SelectMany(t => t.Pilots).ToArray();
  • ToDictionary – crea un’istanza di classe Dictionary; il primo (il nome del team) è la chiave, mentre il secondo è un array di piloti
    Dictionary<string, Pilot[]> dict = (from team in teams where team.Wins > 0 select team).
    ToDictionary(t => t.TeamName, t => t.Pilots);
    foreach (string key in dict.Keys)
    {
    Console.WriteLine(key);
    foreach (Pilot p in dict[key])
    Console.WriteLine($”- {p.LastName}: {p.Point}”);
    }

Il metodo ToLookup crea un’enumerazione, una sorta di dizionario costituito da una sequenza di elementi chiave ai quali corrispondono altre sequenze di elementi. L’esempio precedente può essere replicato come segue:

ILookup<F1Team, Pilot[]> lookup = (from team in teams where team.Wins > 0 select team).ToLookup(t => t, t => t.Pilots);
foreach (var key in lookup)
{
Console.WriteLine($”{key.Key.TeamName}”);
foreach (Pilot p in key.SelectMany(pilots => pilots)) Console.WriteLine($”- {p.LastName}: {p.Point}”);
}

AsEnumerable restituisce la sequenza iniziale sotto forma di un oggetto IEnumerable<T>. OfType restituisce solo gli elementi di un dato tipo, mentre Cast permette di convertireun’enumerazione di elementi in un differente tipo. Tramite Concat possiamo concatenare due sequenze una dopo l’altra, mentre SequenceEqual verifica se due sequenze sono perfettamente uguali.