Nozioni fondamentali sulle espressioni di query
Questo articolo presenta i concetti di base relativi alle espressioni di query nel linguaggio C#.
Che cos'è una query e cosa fa?
Una query è un set di istruzioni che descrive i dati da recuperare da una determinata origine (o più origini) dati e indica quale forma e organizzazione devono avere i dati restituiti. Una query è distinta dai risultati che produce.
In genere, i dati di origine vengono organizzati logicamente come una sequenza di elementi dello stesso tipo. Ad esempio, una tabella di database SQL contiene una sequenza di righe. In un file XML è presente una "sequenza" di elementi XML, anche se gli elementi XML sono organizzati gerarchicamente in una struttura ad albero. Una raccolta in memoria contiene una sequenza di oggetti.
Dal punto di vista di un'applicazione, il tipo e la struttura specifici dei dati di origine non è importante. L'applicazione considera sempre i dati di origine come raccolta IEnumerable<T> o IQueryable<T>. In LINQ to XML, ad esempio, i dati di origine sono resi visibili come oggetto IEnumerable
<XElement>.
Data questa sequenza di origine, una query può eseguire una delle tre operazioni seguenti:
Recuperare un subset di elementi per produrre una nuova sequenza senza modificare i singoli elementi. La query può quindi ordinare o raggruppare la sequenza restituita in vari modi, come illustrato nell'esempio seguente (si presuppone che
scores
sia un elementoint[]
):IEnumerable<int> highScoresQuery = from score in scores where score > 80 orderby score descending select score;
Recuperare una sequenza di elementi come nell'esempio precedente, ma trasformandoli in un nuovo tipo di oggetto. Ad esempio, una query potrebbe recuperare solo i cognomi da determinati record cliente in un'origine dati. Oppure può recuperare il record completo e quindi usarlo per creare un altro tipo di oggetto in memoria o anche dati XML prima di generare la sequenza di risultati finale. L'esempio seguente illustra una proiezione da
int
astring
. Si noti il nuovo tipo dihighScoresQuery
.IEnumerable<string> highScoresQuery2 = from score in scores where score > 80 orderby score descending select $"The score is {score}";
Recuperare un valore singleton sui dati di origine, ad esempio:
Il numero di elementi che corrispondono a una determinata condizione.
L'elemento con il valore massimo o minimo.
Il primo elemento che corrisponde a una condizione o la somma di particolari valori in un set specificato di elementi. Ad esempio, la query seguente restituisce il numero di punteggi superiori a 80 dalla matrice di interi
scores
:var highScoreCount = ( from score in scores where score > 80 select score ).Count();
Nell'esempio precedente si noti l'uso delle parentesi attorno all'espressione di query prima della chiamata al metodo Enumerable.Count. È anche possibile utilizzare una nuova variabile per memorizzare il risultato concreto.
IEnumerable<int> highScoresQuery3 = from score in scores where score > 80 select score; var scoreCount = highScoresQuery3.Count();
Nell'esempio precedente la query viene eseguita nella chiamata a Count
, poiché Count
deve eseguire l'iterazione dei risultati per determinare il numero di elementi restituiti da highScoresQuery
.
Che cos'è un'espressione di query?
Un'espressione di query è una query espressa nella sintassi delle query. È un costrutto di linguaggio di prima classe. È esattamente come qualsiasi altra espressione e può essere usata in qualsiasi contesto in cui un'espressione C# è valida. Un'espressione di query consiste in un set di clausole scritte in una sintassi dichiarativa simile a SQL o XQuery. Ogni clausola contiene a sua volta una o più espressioni C# e queste espressioni possono essere espressioni di query o contenere un'espressione di query.
Un'espressione di query deve iniziare con una clausola from e terminare con una clausola select o group. Tra la prima clausola from
e l'ultima clausola select
o group
può contenere una o più delle seguenti clausole facoltative: where, orderby, join, let e anche altre clausole from. È anche possibile usare la parola chiave into per consentire al risultato di una clausola join
o group
di funzionare come origine per le altre clausole di query nella stessa espressione di query.
Variabile di query
In LINQ una variabile di query è qualsiasi variabile che archivia una query anziché i risultati di una query. In particolare, una variabile di query è sempre un tipo enumerabile che genera una sequenza di elementi quando viene iterato in un'istruzione foreach
o una chiamata diretta al relativo metodo IEnumerator.MoveNext().
Nota
Negli esempi di questo articolo si usano l'origine dati e i dati di esempio seguenti.
record City(string Name, long Population);
record Country(string Name, double Area, long Population, List<City> Cities);
record Product(string Name, string Category);
static readonly City[] cities = [
new City("Tokyo", 37_833_000),
new City("Delhi", 30_290_000),
new City("Shanghai", 27_110_000),
new City("São Paulo", 22_043_000),
new City("Mumbai", 20_412_000),
new City("Beijing", 20_384_000),
new City("Cairo", 18_772_000),
new City("Dhaka", 17_598_000),
new City("Osaka", 19_281_000),
new City("New York-Newark", 18_604_000),
new City("Karachi", 16_094_000),
new City("Chongqing", 15_872_000),
new City("Istanbul", 15_029_000),
new City("Buenos Aires", 15_024_000),
new City("Kolkata", 14_850_000),
new City("Lagos", 14_368_000),
new City("Kinshasa", 14_342_000),
new City("Manila", 13_923_000),
new City("Rio de Janeiro", 13_374_000),
new City("Tianjin", 13_215_000)
];
static readonly Country[] countries = [
new Country ("Vatican City", 0.44, 526, [new City("Vatican City", 826)]),
new Country ("Monaco", 2.02, 38_000, [new City("Monte Carlo", 38_000)]),
new Country ("Nauru", 21, 10_900, [new City("Yaren", 1_100)]),
new Country ("Tuvalu", 26, 11_600, [new City("Funafuti", 6_200)]),
new Country ("San Marino", 61, 33_900, [new City("San Marino", 4_500)]),
new Country ("Liechtenstein", 160, 38_000, [new City("Vaduz", 5_200)]),
new Country ("Marshall Islands", 181, 58_000, [new City("Majuro", 28_000)]),
new Country ("Saint Kitts & Nevis", 261, 53_000, [new City("Basseterre", 13_000)])
];
L'esempio di codice seguente illustra un'espressione di query semplice con un'origine dati, una clausola di filtro, una clausola di ordinamento e nessuna trasformazione degli elementi di origine. La clausola select
termina la query.
// Data source.
int[] scores = [90, 71, 82, 93, 75, 82];
// Query Expression.
IEnumerable<int> scoreQuery = //query variable
from score in scores //required
where score > 80 // optional
orderby score descending // optional
select score; //must end with select or group
// Execute the query to produce the results
foreach (var testScore in scoreQuery)
{
Console.WriteLine(testScore);
}
// Output: 93 90 82 82
Nell'esempio precedente scoreQuery
è una variabile di query, che a volte viene definita semplicemente query. La variabile di query non archivia dati sul risultato effettivo, che vengono generati nel ciclo foreach
. E quando viene eseguita l'istruzione foreach
i risultati della query non vengono restituiti attraverso la variabile di query scoreQuery
. Vengono piuttosto restituiti attraverso la variabile di iterazione testScore
. La variabile scoreQuery
può essere iterata in un secondo ciclo foreach
. Genera gli stessi risultati purché non siano state modificate né la variabile né l'origine dati.
Una variabile di query può archiviare una query espressa nella sintassi di query, nella sintassi di metodo o in una combinazione delle due. Negli esempi seguenti sia queryMajorCities
che queryMajorCities2
sono variabili di query:
City[] cities = [
new City("Tokyo", 37_833_000),
new City("Delhi", 30_290_000),
new City("Shanghai", 27_110_000),
new City("São Paulo", 22_043_000)
];
//Query syntax
IEnumerable<City> queryMajorCities =
from city in cities
where city.Population > 100000
select city;
// Execute the query to produce the results
foreach (City city in queryMajorCities)
{
Console.WriteLine(city);
}
// Output:
// City { Population = 120000 }
// City { Population = 112000 }
// City { Population = 150340 }
// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);
I due esempi seguenti illustrano invece le variabili che non sono variabili di query anche se ognuna viene inizializzata con una query. Non sono variabili di query perché archiviano i risultati:
var highestScore = (
from score in scores
select score
).Max();
// or split the expression
IEnumerable<int> scoreQuery =
from score in scores
select score;
var highScore = scoreQuery.Max();
// the following returns the same result
highScore = scores.Max();
var largeCitiesList = (
from country in countries
from city in country.Cities
where city.Population > 10000
select city
).ToList();
// or split the expression
IEnumerable<City> largeCitiesQuery =
from country in countries
from city in country.Cities
where city.Population > 10000
select city;
var largeCitiesList2 = largeCitiesQuery.ToList();
Tipizzazione esplicita e implicita delle variabili di query
In questa documentazione viene usato in genere il tipo esplicito della variabile di query allo scopo di evidenziare la relazione di tipo tra la variabile di query e la clausola select. Tuttavia, è possibile usare anche la parola chiave var per indicare al compilatore di dedurre il tipo di una variabile di query, o qualsiasi altra variabile locale, in fase di compilazione. L'esempio di query illustrato in precedenza in questo articolo può essere espresso anche usando la tipizzazione implicita:
var queryCities =
from city in cities
where city.Population > 100000
select city;
Nell'esempio precedente l'uso di var è facoltativo. queryCities
è un IEnumerable<City>
che indica se tipizzato in modo implicito o esplicito.
Avviare un'espressione di query
Un'espressione di query deve iniziare con una clausola from
. Specifica un'origine dati insieme a una variabile di intervallo. La variabile di intervallo rappresenta ogni elemento successivo nella sequenza di origine man mano che si attraversa la sequenza di origine. La variabile di intervallo è fortemente tipizzata in base al tipo di elementi nell'origine dati. Nell'esempio seguente, poiché countries
è una matrice di oggetti Country
, anche la variabile di intervallo è tipizzata come Country
. Poiché la variabile di intervallo è fortemente tipizzata, è possibile usare l'operatore punto per accedere ai membri disponibili del tipo.
IEnumerable<Country> countryAreaQuery =
from country in countries
where country.Area > 500000 //sq km
select country;
La variabile di intervallo è nell'ambito finché la query viene terminata con un punto e virgola o con una clausola continuation.
Un'espressione di query può contenere più clausole from
. Usare altre clausole from
quando ogni elemento nella sequenza di origine è a sua volta una raccolta o contiene una raccolta. Ad esempio, si supponga di avere una raccolta di oggetti Country
, ognuna delle quali contiene una raccolta di oggetti City
denominata Cities
. Per eseguire query sugli oggetti City
in ogni Country
, usare due clausole from
come illustrato di seguito:
IEnumerable<City> cityQuery =
from country in countries
from city in country.Cities
where city.Population > 10000
select city;
Per altre informazioni, vedere Clausola from.
Terminare un'espressione di query
Un'espressione di query deve terminare con una clausola group
o una clausola select
.
Clausola group
Usare la clausola group
per produrre una sequenza di gruppi organizzata in base a una chiave specificata. La chiave può essere qualsiasi tipo di dati. Ad esempio, la query seguente crea una sequenza di gruppi che contiene uno o più oggetti Country
e la cui chiave è un tipo con valore char
corrispondente alla prima lettera dei nomi dei paesi.
var queryCountryGroups =
from country in countries
group country by country.Name[0];
Per altre informazioni sul raggruppamento, vedere Clausola group.
Clausola select
Usare la clausola select
per creare tutti gli altri tipi di sequenze. Una clausola select
semplice produce una sequenza usando lo stesso tipo di oggetti dell'origine dati. In questo esempio l'origine dati contiene oggetti Country
. La clausola orderby
si limita a ordinare gli elementi in base a un nuovo ordine e la clausola select
produce una sequenza degli oggetti Country
riordinati.
IEnumerable<Country> sortedQuery =
from country in countries
orderby country.Area
select country;
La clausola select
può essere usata per trasformare i dati di origine in sequenze di nuovi tipi. Questa trasformazione è detta anche proiezione. Nell'esempio seguente la clausola select
proietta una sequenza di tipi anonimi che contiene solo un subset dei campi dell'elemento originale. I nuovi oggetti vengono inizializzati usando un inizializzatore di oggetto.
var queryNameAndPop =
from country in countries
select new
{
Name = country.Name,
Pop = country.Population
};
Pertanto, in questo esempio, var
è necessario perché la query produce un tipo anonimo.
Per altre informazioni su tutti i modi in cui una clausola select
può essere usata per trasformare i dati di origine, vedere Clausola select.
Continuazioni con into
È possibile usare la parola chiave into
in una clausola select
o group
per creare un identificatore temporaneo che archivia una query. Usare la clausola into
quando è necessario eseguire operazioni di query aggiuntive per una query dopo un'operazione di raggruppamento o selezione. Nell'esempio seguente, countries
indica i paesi raggruppati in base alla popolazione in intervalli di 10 milioni. Dopo avere creato questi gruppi, le altre clausole escludono alcuni gruppi, quindi ordinano i gruppi in ordine crescente. Per eseguire le operazioni extra, è richiesta la continuazione rappresentata da countryGroup
.
// percentileQuery is an IEnumerable<IGrouping<int, Country>>
var percentileQuery =
from country in countries
let percentile = (int)country.Population / 10_000_000
group country by percentile into countryGroup
where countryGroup.Key >= 20
orderby countryGroup.Key
select countryGroup;
// grouping is an IGrouping<int, Country>
foreach (var grouping in percentileQuery)
{
Console.WriteLine(grouping.Key);
foreach (var country in grouping)
{
Console.WriteLine(country.Name + ":" + country.Population);
}
}
Per altre informazioni, vedere into.
Filtro, ordinamento e join
Tra la clausola iniziale from
e la clausola finale select
o group
, tutte le altre clausole (where
, join
, orderby
, from
, let
) sono facoltative. Qualsiasi clausola facoltativa può essere usata zero o più volte nel corpo di una query.
Clausola where
Usare la clausola where
per escludere gli elementi dai dati di origine in base a una o più espressioni del predicato. La clausola where
nell'esempio seguente include un predicato con due condizioni.
IEnumerable<City> queryCityPop =
from city in cities
where city.Population is < 200000 and > 100000
select city;
Per altre informazioni, vedere Clausola where.
Clausola orderby
Usare la clausola orderby
per ordinare i risultati in ordine crescente o decrescente. È anche possibile specificare gli ordinamenti secondari. Nell'esempio seguente viene eseguito un ordinamento primario per gli oggetti country
usando la proprietà Area
. Viene quindi eseguito un ordinamento secondario usando la proprietà Population
.
IEnumerable<Country> querySortedCountries =
from country in countries
orderby country.Area, country.Population descending
select country;
La parola chiave ascending
è facoltativa, ma consente l'ordinamento predefinito se non viene specificato alcun ordine. Per altre informazioni, vedere Clausola orderby.
Clausola join
Usare la clausola join
per associare e/o combinare gli elementi di un'origine dati con gli elementi di un'altra origine dati in base a un confronto di uguaglianza tra le chiavi specificate in ogni elemento. In LINQ le operazioni di join vengono eseguite su sequenze di oggetti i cui elementi sono tipi diversi. Dopo avere unito due sequenze, è necessario usare un'istruzione select
o group
per specificare l'elemento da archiviare nella sequenza di output. È anche possibile usare un tipo anonimo per combinare le proprietà da ogni set di elementi associati in un nuovo tipo per la sequenza di output. L'esempio seguente associa oggetti prod
la cui proprietà Category
corrisponde a una delle categorie nella matrice di stringhe categories
. I prodotti il cui valore Category
non corrisponde a una delle stringhe in categories
vengono esclusi. L'istruzione select
proietta un nuovo tipo le cui proprietà sono accettate sia da cat
che da prod
.
var categoryQuery =
from cat in categories
join prod in products on cat equals prod.Category
select new
{
Category = cat,
Name = prod.Name
};
È anche possibile creare un join di gruppo archiviando i risultati dell'operazione join
in una variabile temporanea usando la parola chiave into. Per altre informazioni, vedere Clausola join.
Clausola let
Usare la clausola let
per archiviare il risultato di un'espressione, ad esempio una chiamata al metodo, in una nuova variabile di intervallo. Nell'esempio seguente la variabile di intervallo firstName
archivia il primo elemento della matrice di stringhe restituita da Split
.
string[] names = ["Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia"];
IEnumerable<string> queryFirstNames =
from name in names
let firstName = name.Split(' ')[0]
select firstName;
foreach (var s in queryFirstNames)
{
Console.Write(s + " ");
}
//Output: Svetlana Claire Sven Cesar
Per altre informazioni, vedere Clausola let.
Sottoquery in un'espressione di query
Una clausola di query può contenere un'espressione di query, a volte detta sottoquery. Ogni sottoquery inizia con la propria clausola from
che non fa necessariamente riferimento alla stessa origine dati nella prima clausola from
. Ad esempio, la query seguente rappresenta un'espressione di query usate nell'istruzione select per recuperare i risultati di un'operazione di raggruppamento.
var queryGroupMax =
from student in students
group student by student.Year into studentGroup
select new
{
Level = studentGroup.Key,
HighestScore = (
from student2 in studentGroup
select student2.ExamScores.Average()
).Max()
};
Per altre informazioni, vedere Eseguire una sottoquery su un'operazione di raggruppamento.