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 elemento int[]):

    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 a string. Si noti il nuovo tipo di highScoresQuery.

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

Vedi anche